@kici-dev/sdk 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/context.d.ts CHANGED
@@ -19,6 +19,20 @@ export interface JobInfo {
19
19
  name: string;
20
20
  runsOn: string;
21
21
  }
22
+ /**
23
+ * Facts about the agent a `runsOnAll` host-fanout child is pinned to, exposed
24
+ * as `ctx.agent`. Present only for jobs that use `runsOnAll`.
25
+ */
26
+ export interface AgentInfo {
27
+ /** Hostname of the agent. */
28
+ host: string;
29
+ /** The agent's label set. */
30
+ labels: readonly string[];
31
+ /** Operating-system platform (os.platform()). */
32
+ platform?: string;
33
+ /** CPU architecture (os.arch()). */
34
+ arch?: string;
35
+ }
22
36
  /**
23
37
  * Outputs of a matrix job as seen by a downstream `needs:` consumer.
24
38
  * A downstream that consumes a matrix upstream receives this envelope instead of
@@ -31,7 +45,26 @@ export interface MatrixJobOutputs<T = Record<string, unknown>> {
31
45
  merged: T;
32
46
  }
33
47
  /** Runtime discriminator: true when `ctx.jobOutputs(ref)` returned a matrix envelope. */
34
- export declare function isMatrixJobOutputs(value: Record<string, unknown> | MatrixJobOutputs): value is MatrixJobOutputs;
48
+ export declare function isMatrixJobOutputs(value: Record<string, unknown> | MatrixJobOutputs | HostJobOutputs): value is MatrixJobOutputs;
49
+ /**
50
+ * Outputs of a `runsOnAll` host-fanout job as seen by a downstream `needs:`
51
+ * consumer. Keyed by hostname. Unlike {@link MatrixJobOutputs}, the summary does
52
+ * NOT collapse to a last-write-wins scalar (a fleet footgun): `summary.outputs`
53
+ * is an array view across hosts, and `succeededHosts`/`failedHosts` name the
54
+ * per-host outcome.
55
+ */
56
+ export interface HostJobOutputs<T = Record<string, unknown>> {
57
+ /** Keyed by hostname. */
58
+ byHost: Record<string, T>;
59
+ summary: {
60
+ succeededHosts: string[];
61
+ failedHosts: string[];
62
+ /** Per output key, every host's value (array view; never a collapsing scalar). */
63
+ outputs: Record<string, unknown[]>;
64
+ };
65
+ }
66
+ /** Runtime discriminator: true when `ctx.jobOutputs(ref)` returned a host envelope. */
67
+ export declare function isHostJobOutputs(value: Record<string, unknown> | MatrixJobOutputs | HostJobOutputs): value is HostJobOutputs;
35
68
  /**
36
69
  * Augmentable interface for known secret keys.
37
70
  * When augmented via `kici types` (.d.ts generation), narrows get/expose key parameter.
@@ -93,6 +126,17 @@ export interface StepContext<TInputs = Record<string, unknown>> {
93
126
  * - Undefined for jobs without matrix configuration
94
127
  */
95
128
  matrix?: MatrixValues;
129
+ /**
130
+ * Hostname of the agent this job instance is running on.
131
+ * Set only for jobs that use `runsOnAll` (host fan-out — one pinned execution
132
+ * per matching host). Undefined for jobs without host fan-out.
133
+ */
134
+ host?: string;
135
+ /**
136
+ * Facts about the agent this job instance is pinned to (hostname, labels,
137
+ * platform, arch). Set only for jobs that use `runsOnAll`. Undefined otherwise.
138
+ */
139
+ agent?: AgentInfo;
96
140
  /**
97
141
  * Raw webhook payload from the git provider.
98
142
  * Contains the full, unmodified payload as received from the webhook.
@@ -186,7 +230,7 @@ export interface StepContext<TInputs = Record<string, unknown>> {
186
230
  */
187
231
  jobOutputs(ref: {
188
232
  name: string;
189
- }): Record<string, unknown> | MatrixJobOutputs;
233
+ }): Record<string, unknown> | MatrixJobOutputs | HostJobOutputs;
190
234
  /**
191
235
  * Publish a secret output value from this job.
192
236
  * Secret outputs are encrypted before leaving the agent and can be consumed
package/dist/context.js CHANGED
@@ -4,7 +4,11 @@ import "./chunk-BTugEXQM.js";
4
4
  function isMatrixJobOutputs(value) {
5
5
  return typeof value === "object" && value !== null && "byMatrix" in value && "merged" in value && typeof value.byMatrix === "object";
6
6
  }
7
+ /** Runtime discriminator: true when `ctx.jobOutputs(ref)` returned a host envelope. */
8
+ function isHostJobOutputs(value) {
9
+ return typeof value === "object" && value !== null && "byHost" in value && "summary" in value && typeof value.byHost === "object";
10
+ }
7
11
  //#endregion
8
- export { isMatrixJobOutputs };
12
+ export { isHostJobOutputs, isMatrixJobOutputs };
9
13
 
10
14
  //# sourceMappingURL=context.js.map
package/dist/index.d.ts CHANGED
@@ -14,8 +14,10 @@ export type { Rule, RuleContext, RuleCheckFn, RuleResult, EventPayload, RuleEval
14
14
  export { validateDag } from './validation/index.js';
15
15
  export type { DagNode, DagValidationResult } from './validation/index.js';
16
16
  export type { SourceLocation, OutputProxy, Step, StepOptions, StepRunFn, BareStepFn, StepInput, OutputSchema, InferOutputs, Job, JobOptions, GenericInitConfig, MiseInitConfig, InitPreset, InitItem, InitConfig, ContainerConfig, RunsOnSelector, RunsOn, Workflow, WorkflowOptions, Registry, Trigger, DynamicJobFn, DynamicJobContext, JobOrFactory, } from './types.js';
17
- export { isDynamicJobFn, dynamicJob, getDynamicJobGroup, DYNAMIC_JOB_GROUP_TAG } from './types.js';
18
- export type { TaggedDynamicJobFn } from './types.js';
17
+ export { isDynamicJobFn, dynamicJob, getDynamicJobGroup, getDynamicJobNeeds, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, } from './types.js';
18
+ export type { TaggedDynamicJobFn, ResultAwareDynamicJobConfig, ResultAwareDynamicJobFn, DynamicJobNeed, } from './types.js';
19
+ export { buildNeedsContext } from './needs-context.js';
20
+ export type { UpstreamSnapshot, NeedsContext, NeedEntry, GroupNeedEntry } from './needs-context.js';
19
21
  export { CacheSpecSchema, normalizeCacheSpecs } from './cache-types.js';
20
22
  export type { CacheSpec, CacheInput } from './cache-types.js';
21
23
  export type { CacheRestoreResult, CacheApi } from './cache-types.js';
@@ -23,14 +25,14 @@ export { provenanceSubjectIsPath } from './provenance-types.js';
23
25
  export type { AttestProvenanceOptions, AttestProvenanceResult, ProvenanceSubjectInput, } from './provenance-types.js';
24
26
  export { dynamicGroup, isDynamicGroupRef, DYNAMIC_GROUP_TAG } from './dynamic-group.js';
25
27
  export type { DynamicGroupRef } from './dynamic-group.js';
26
- export type { StepContext, Logger, WorkflowInfo, JobInfo, MatrixJobOutputs, RepoInfo, StepSecretsTyped, KnownSecretKeys, } from './context.js';
27
- export { isMatrixJobOutputs } from './context.js';
28
+ export type { StepContext, Logger, WorkflowInfo, JobInfo, AgentInfo, MatrixJobOutputs, HostJobOutputs, RepoInfo, StepSecretsTyped, KnownSecretKeys, } from './context.js';
29
+ export { isMatrixJobOutputs, isHostJobOutputs } from './context.js';
28
30
  export { buildKiciApi } from './api-types.js';
29
31
  export type { KiciApi, KiciApiTransport, InfrastructureApi, InfrastructureListResult, } from './api-types.js';
30
32
  export { SecretNotFoundError } from './errors.js';
31
33
  export { createStepSecrets } from './secrets.js';
32
34
  export type { StepSecrets, TrackedStepSecrets, SecretMeta, SecretFileOptions, MountedFile, StepSecretsFileHost, StepSecretsFileWiring, StepSecretsHandle, StepSecretMountKind, StepSecretMountRecord, } from './secrets.js';
33
- export { createStepOutputProxy, createJobOutputProxy, resolveStepOutputs, resolveJobOutputs, setStepOutputsMap, setJobOutputsMap, setStepRefMap, getStepOutputsMap, getJobOutputsMap, getStepRefMap, } from './outputs.js';
35
+ export { createStepOutputProxy, createJobOutputProxy, createSnapshotOutputProxy, resolveStepOutputs, resolveJobOutputs, setStepOutputsMap, setJobOutputsMap, setStepRefMap, getStepOutputsMap, getJobOutputsMap, getStepRefMap, } from './outputs.js';
34
36
  export type { OutputsMap, StepRefMap } from './outputs.js';
35
37
  export type { StaticMatrixArray, StaticMatrixObject, DynamicMatrixFn, DynamicMatrixContext, Matrix, MatrixInclude, MatrixExclude, MatrixValues, } from './matrix/index.js';
36
38
  export { isStaticArray, isStaticObject, isDynamicFunction } from './matrix/index.js';
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import "./chunk-BTugEXQM.js";
2
2
  import { buildKiciApi } from "./api-types.js";
3
3
  import { normalizeRequireApproval } from "./approval.js";
4
4
  import { CacheSpecSchema, normalizeCacheSpecs } from "./cache-types.js";
5
- import { isMatrixJobOutputs } from "./context.js";
5
+ import { isHostJobOutputs, isMatrixJobOutputs } from "./context.js";
6
6
  import { DYNAMIC_GROUP_TAG, dynamicGroup, isDynamicGroupRef } from "./dynamic-group.js";
7
7
  import { SecretNotFoundError } from "./errors.js";
8
8
  import { fixture } from "./fixture.js";
9
- import { createJobOutputProxy, createStepOutputProxy, getJobOutputsMap, getStepOutputsMap, getStepRefMap, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "./outputs.js";
9
+ import { createJobOutputProxy, createSnapshotOutputProxy, createStepOutputProxy, getJobOutputsMap, getStepOutputsMap, getStepRefMap, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "./outputs.js";
10
10
  import { step } from "./step.js";
11
11
  import { idempotent, idempotentStep } from "./idempotent.js";
12
12
  import { job } from "./job.js";
@@ -41,7 +41,8 @@ import { isEventType } from "./events/event-payloads.js";
41
41
  import "./rules/index.js";
42
42
  import { validateDag } from "./validation/dag.js";
43
43
  import "./validation/index.js";
44
- import { DYNAMIC_JOB_GROUP_TAG, dynamicJob, getDynamicJobGroup, isDynamicJobFn } from "./types.js";
44
+ import { DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, dynamicJob, getDynamicJobGroup, getDynamicJobNeeds, isDynamicJobFn } from "./types.js";
45
+ import { buildNeedsContext } from "./needs-context.js";
45
46
  import { provenanceSubjectIsPath } from "./provenance-types.js";
46
47
  import { createStepSecrets } from "./secrets.js";
47
48
  import { isDynamicFunction, isStaticArray, isStaticObject } from "./matrix/types.js";
@@ -51,4 +52,4 @@ import { defineEvent } from "./events/define-event.js";
51
52
  import "./events/index.js";
52
53
  import { WaitForTimeoutError, waitFor, waitForStep } from "./wait-for.js";
53
54
  import { z } from "zod";
54
- export { CacheSpecSchema, DYNAMIC_GROUP_TAG, DYNAMIC_JOB_GROUP_TAG, SecretNotFoundError, WaitForTimeoutError, afterStep, applyIncludeExclude, beforeStep, buildKiciApi, cleanup, comment, create, createJobOutputProxy, createStepOutputProxy, createStepSecrets, defineEvent, del as delete, dispatch, dynamicGroup, dynamicJob, evaluateRules, expandMatrix, fixture, fork, genericWebhook, getDynamicJobGroup, getJobOutputsMap, getStepOutputsMap, getStepRefMap, idempotent, idempotentStep, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isEventType, isMatrixJobOutputs, isStaticArray, isStaticObject, job, jobComplete, kiciEvent, lifecycle, normalizeCacheSpecs, normalizeRequireApproval, onCancel, onFailure, onSuccess, pr, provenanceSubjectIsPath, push, release, resolveJobOutputs, resolveStepOutputs, review, reviewComment, rule, schedule, setJobOutputsMap, setStepOutputsMap, setStepRefMap, skip, star, status, step, tag, validateDag, waitFor, waitForStep, watch, webhook, workflow, workflowComplete, workflowRun, z };
55
+ export { CacheSpecSchema, DYNAMIC_GROUP_TAG, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, SecretNotFoundError, WaitForTimeoutError, afterStep, applyIncludeExclude, beforeStep, buildKiciApi, buildNeedsContext, cleanup, comment, create, createJobOutputProxy, createSnapshotOutputProxy, createStepOutputProxy, createStepSecrets, defineEvent, del as delete, dispatch, dynamicGroup, dynamicJob, evaluateRules, expandMatrix, fixture, fork, genericWebhook, getDynamicJobGroup, getDynamicJobNeeds, getJobOutputsMap, getStepOutputsMap, getStepRefMap, idempotent, idempotentStep, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isEventType, isHostJobOutputs, isMatrixJobOutputs, isStaticArray, isStaticObject, job, jobComplete, kiciEvent, lifecycle, normalizeCacheSpecs, normalizeRequireApproval, onCancel, onFailure, onSuccess, pr, provenanceSubjectIsPath, push, release, resolveJobOutputs, resolveStepOutputs, review, reviewComment, rule, schedule, setJobOutputsMap, setStepOutputsMap, setStepRefMap, skip, star, status, step, tag, validateDag, waitFor, waitForStep, watch, webhook, workflow, workflowComplete, workflowRun, z };
package/dist/job.js CHANGED
@@ -32,10 +32,17 @@ function job(nameOrOptions, maybeOptions) {
32
32
  steps = [options.run];
33
33
  }
34
34
  validateInit(options.init, name);
35
+ if (options.runsOn !== void 0 && options.runsOnAll !== void 0) throw new Error(`job('${name}'): runsOn and runsOnAll are mutually exclusive`);
36
+ if (options.runsOn === void 0 && options.runsOnAll === void 0) throw new Error(`job('${name}'): one of runsOn or runsOnAll is required`);
37
+ if (options.onUnreachable !== void 0 && options.runsOnAll === void 0) console.warn(`[kici] job('${name}'): onUnreachable is ignored without runsOnAll`);
35
38
  return {
36
39
  _tag: "Job",
37
40
  name,
38
- runsOn: options.runsOn,
41
+ ...options.runsOn !== void 0 && { runsOn: options.runsOn },
42
+ ...options.runsOnAll !== void 0 && { runsOnAll: options.runsOnAll },
43
+ ...options.onUnreachable !== void 0 && { onUnreachable: options.onUnreachable },
44
+ ...options.maxParallel !== void 0 && { maxParallel: options.maxParallel },
45
+ ...options.failFast !== void 0 && { failFast: options.failFast },
39
46
  steps,
40
47
  needs: options.needs,
41
48
  rules: options.rules,
@@ -0,0 +1,32 @@
1
+ import type { OutputProxy, DynamicJobNeed } from './types.js';
2
+ /**
3
+ * Frozen snapshot of upstream outputs, captured once at first eval of a
4
+ * result-aware dynamic generator and replayed unchanged on re-eval.
5
+ *
6
+ * - `jobs` maps an upstream job name to its outputs record.
7
+ * - `groups` maps a dynamic group name to its ordered member job names.
8
+ */
9
+ export interface UpstreamSnapshot {
10
+ jobs: Record<string, Record<string, unknown>>;
11
+ groups: Record<string, string[]>;
12
+ }
13
+ /** One entry in the array exposed for a `dynamicGroup(...)` need. */
14
+ export interface GroupNeedEntry {
15
+ name: string;
16
+ result: OutputProxy<any>;
17
+ }
18
+ /** A single-job need exposes `{ result }`; a group need exposes an ordered array. */
19
+ export type NeedEntry = {
20
+ result: OutputProxy<any>;
21
+ } | GroupNeedEntry[];
22
+ /** The resolved `ctx.needs` map keyed by job name or group name. */
23
+ export type NeedsContext = Record<string, NeedEntry>;
24
+ /**
25
+ * Resolve declared needs against a frozen snapshot into the `ctx.needs` map.
26
+ *
27
+ * - A single static/named-job need resolves to `{ result: <proxy over jobs[name]> }`.
28
+ * - A `dynamicGroup(...)` need resolves to an ordered array of `{ name, result }`,
29
+ * one entry per group member in the snapshot's deterministic eval order.
30
+ */
31
+ export declare function buildNeedsContext(snapshot: UpstreamSnapshot, declaredNeeds: ReadonlyArray<DynamicJobNeed>): NeedsContext;
32
+ //# sourceMappingURL=needs-context.d.ts.map
@@ -0,0 +1,44 @@
1
+ import "./chunk-BTugEXQM.js";
2
+ import { createSnapshotOutputProxy } from "./outputs.js";
3
+ //#region src/needs-context.ts
4
+ function needKey(need) {
5
+ if (typeof need === "string") return {
6
+ kind: "job",
7
+ key: need
8
+ };
9
+ if ("group" in need) return {
10
+ kind: "group",
11
+ key: need.group
12
+ };
13
+ if ("name" in need) return {
14
+ kind: "job",
15
+ key: need.name
16
+ };
17
+ return {
18
+ kind: "job",
19
+ key: need.name
20
+ };
21
+ }
22
+ /**
23
+ * Resolve declared needs against a frozen snapshot into the `ctx.needs` map.
24
+ *
25
+ * - A single static/named-job need resolves to `{ result: <proxy over jobs[name]> }`.
26
+ * - A `dynamicGroup(...)` need resolves to an ordered array of `{ name, result }`,
27
+ * one entry per group member in the snapshot's deterministic eval order.
28
+ */
29
+ function buildNeedsContext(snapshot, declaredNeeds) {
30
+ const out = {};
31
+ for (const need of declaredNeeds) {
32
+ const { kind, key } = needKey(need);
33
+ if (kind === "group") out[key] = (snapshot.groups[key] ?? []).map((name) => ({
34
+ name,
35
+ result: createSnapshotOutputProxy(name, snapshot.jobs[name])
36
+ }));
37
+ else out[key] = { result: createSnapshotOutputProxy(key, snapshot.jobs[key]) };
38
+ }
39
+ return out;
40
+ }
41
+ //#endregion
42
+ export { buildNeedsContext };
43
+
44
+ //# sourceMappingURL=needs-context.js.map
package/dist/outputs.d.ts CHANGED
@@ -61,6 +61,18 @@ export declare function createStepOutputProxy<T>(stepName: string): OutputProxy<
61
61
  * @returns A Proxy that resolves property access at runtime
62
62
  */
63
63
  export declare function createJobOutputProxy(jobName: string): OutputProxy<any>;
64
+ /**
65
+ * Output proxy bound to a specific outputs object (not the module-global map).
66
+ *
67
+ * Used to expose a frozen upstream-job snapshot as `ctx.needs.<job>.result` for
68
+ * result-aware dynamic generators. The snapshot is captured once at eval and
69
+ * replayed unchanged on re-eval, so the proxy reads a fixed `outputs` object
70
+ * rather than the live module-global job-outputs map.
71
+ *
72
+ * @param jobName - The upstream job whose outputs to proxy (used in error text)
73
+ * @param outputs - The frozen outputs object, or undefined if the job produced none
74
+ */
75
+ export declare function createSnapshotOutputProxy(jobName: string, outputs: Record<string, unknown> | undefined): OutputProxy<any>;
64
76
  /**
65
77
  * Resolve step outputs by step reference (Step object or bare function).
66
78
  * Used by ctx.outputsOf() implementation.
package/dist/outputs.js CHANGED
@@ -140,6 +140,41 @@ function createJobOutputProxy(jobName) {
140
140
  });
141
141
  }
142
142
  /**
143
+ * Output proxy bound to a specific outputs object (not the module-global map).
144
+ *
145
+ * Used to expose a frozen upstream-job snapshot as `ctx.needs.<job>.result` for
146
+ * result-aware dynamic generators. The snapshot is captured once at eval and
147
+ * replayed unchanged on re-eval, so the proxy reads a fixed `outputs` object
148
+ * rather than the live module-global job-outputs map.
149
+ *
150
+ * @param jobName - The upstream job whose outputs to proxy (used in error text)
151
+ * @param outputs - The frozen outputs object, or undefined if the job produced none
152
+ */
153
+ function createSnapshotOutputProxy(jobName, outputs) {
154
+ return new Proxy({}, {
155
+ get(_target, prop, receiver) {
156
+ if (typeof prop === "symbol") return Reflect.get(_target, prop, receiver);
157
+ if (WELL_KNOWN_STRING_PROPS.has(prop)) return Reflect.get(_target, prop, receiver);
158
+ if (!outputs) throw new Error(`Upstream job '${jobName}' has no frozen outputs in the generator snapshot`);
159
+ return outputs[prop];
160
+ },
161
+ ownKeys() {
162
+ return outputs ? Reflect.ownKeys(outputs) : [];
163
+ },
164
+ has(_target, prop) {
165
+ return outputs ? prop in outputs : false;
166
+ },
167
+ getOwnPropertyDescriptor(_target, prop) {
168
+ if (!outputs) return void 0;
169
+ if (prop in outputs) return {
170
+ configurable: true,
171
+ enumerable: true,
172
+ value: outputs[prop]
173
+ };
174
+ }
175
+ });
176
+ }
177
+ /**
143
178
  * Resolve step outputs by step reference (Step object or bare function).
144
179
  * Used by ctx.outputsOf() implementation.
145
180
  *
@@ -175,6 +210,6 @@ function resolveJobOutputs(ref, outputsMap) {
175
210
  return outputs;
176
211
  }
177
212
  //#endregion
178
- export { createJobOutputProxy, createStepOutputProxy, getJobOutputsMap, getStepOutputsMap, getStepRefMap, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap };
213
+ export { createJobOutputProxy, createSnapshotOutputProxy, createStepOutputProxy, getJobOutputsMap, getStepOutputsMap, getStepRefMap, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap };
179
214
 
180
215
  //# sourceMappingURL=outputs.js.map
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { z } from 'zod';
2
2
  import type { $ as Shell } from 'zx';
3
- import type { ResourceRequest } from '@kici-dev/engine';
3
+ import type { ResourceRequest, RunsOnAllInput, OnUnreachableMode } from '@kici-dev/engine';
4
4
  import type { StepContext, Logger } from './context.js';
5
5
  import type { TriggerConfig } from './triggers/types.js';
6
6
  import type { Rule } from './rules/types.js';
@@ -10,7 +10,7 @@ import type { KiciApi } from './api-types.js';
10
10
  import type { DynamicGroupRef } from './dynamic-group.js';
11
11
  import type { EventPayload } from './events/event-payloads.js';
12
12
  import type { RequireApproval } from './approval.js';
13
- export type { ResourceRequest, ResourceSpec } from '@kici-dev/engine';
13
+ export type { ResourceRequest, ResourceSpec, RunsOnAllInput, OnUnreachableMode, } from '@kici-dev/engine';
14
14
  /** Source location captured at a step() call site. */
15
15
  export interface SourceLocation {
16
16
  readonly file: string;
@@ -108,13 +108,16 @@ export type Trigger = TriggerConfig;
108
108
  * the re-evaluation must produce a job with the same name and the same steps.
109
109
  *
110
110
  * - `ctx.event` is deterministic — frozen from the original webhook payload.
111
+ * - `ctx.needs` is deterministic — a snapshot of upstream outputs frozen at first
112
+ * eval and replayed unchanged on re-eval, like `ctx.event`. Present only on
113
+ * result-aware generators created via `dynamicJob(group, { needs, generate })`.
111
114
  * - `$`, `env`, and `kici` can return different results between eval and re-eval
112
115
  * (different agent, different time, different infrastructure state).
113
116
  *
114
- * **Guidance:** derive job names and structure from `ctx.event` data whenever
115
- * possible. If you use `kici.infrastructure.list()` or shell commands to
116
- * determine job names, be aware that changes between eval and re-eval will
117
- * cause a non-determinism warning (or failure if a job disappears).
117
+ * **Guidance:** derive job names and structure from `ctx.event` / `ctx.needs`
118
+ * data whenever possible. If you use `kici.infrastructure.list()` or shell
119
+ * commands to determine job names, be aware that changes between eval and re-eval
120
+ * will cause a non-determinism warning (or failure if a job disappears).
118
121
  */
119
122
  export interface DynamicJobContext {
120
123
  /** zx shell executor for running commands */
@@ -126,6 +129,12 @@ export interface DynamicJobContext {
126
129
  };
127
130
  /** Normalized event envelope that triggered this run. */
128
131
  event?: EventPayload;
132
+ /**
133
+ * Frozen outputs of declared upstream needs (result-aware generators only).
134
+ * Single-job needs expose `ctx.needs.<job>.result.<field>`; group needs
135
+ * expose an ordered array of `{ name, result }`.
136
+ */
137
+ needs?: import('./needs-context.js').NeedsContext;
129
138
  };
130
139
  /** Structured logger */
131
140
  log: Logger;
@@ -167,16 +176,56 @@ export interface TaggedDynamicJobFn extends DynamicJobFn {
167
176
  * Tag a DynamicJobFn with a group name for cross-domain needs.
168
177
  * Static jobs can then depend on this group via `needs: [dynamicGroup('name')]`.
169
178
  *
179
+ * Two forms:
180
+ * - **Function form** (event-only): `dynamicJob('shards', async ({ ctx }) => [...])`.
181
+ * Dispatched at webhook time; deterministic from `ctx.event` alone.
182
+ * - **Options form** (result-aware): `dynamicJob('reports', { needs, generate })`.
183
+ * Deferred until every job/group in `needs` completes, then `generate` is
184
+ * evaluated with the upstreams' frozen outputs available as `ctx.needs`.
185
+ *
170
186
  * @param groupName - The group name (must match what static jobs reference)
171
- * @param fn - The dynamic job generator function
187
+ * @param fnOrConfig - The generator function, or a result-aware `{ needs, generate }` config
172
188
  */
173
- export declare function dynamicJob(groupName: string, fn: DynamicJobFn): TaggedDynamicJobFn;
189
+ export declare function dynamicJob(groupName: string, fnOrConfig: DynamicJobFn | ResultAwareDynamicJobConfig): TaggedDynamicJobFn;
174
190
  /**
175
191
  * Get the group name from a DynamicJobFn, if it was tagged with dynamicJob().
176
192
  * Returns undefined for untagged functions.
177
193
  */
178
194
  export declare function getDynamicJobGroup(fn: DynamicJobFn): string | undefined;
179
195
  export { DYNAMIC_JOB_GROUP_TAG };
196
+ declare const DYNAMIC_JOB_NEEDS_TAG: unique symbol;
197
+ /**
198
+ * One declared upstream edge for a result-aware generator.
199
+ * Same shape as the elements of {@link JobOptions.needs}.
200
+ */
201
+ export type DynamicJobNeed = Job | string | DynamicGroupRef | {
202
+ name: string;
203
+ ifFailed?: 'skip' | 'run';
204
+ } | {
205
+ group: string;
206
+ ifFailed?: 'skip' | 'run';
207
+ };
208
+ /**
209
+ * Options-object form of {@link dynamicJob}: a result-aware generator that is
210
+ * deferred until its declared `needs` complete, then evaluated with their frozen
211
+ * outputs available as `ctx.needs`.
212
+ */
213
+ export interface ResultAwareDynamicJobConfig {
214
+ /** Upstream jobs/groups whose outputs the generator reads via `ctx.needs`. */
215
+ needs: ReadonlyArray<DynamicJobNeed>;
216
+ /** The generator. Receives `ctx.needs` resolved from the frozen upstream snapshot. */
217
+ generate: DynamicJobFn;
218
+ }
219
+ /** A {@link DynamicJobFn} tagged with declared needs (result-aware). */
220
+ export interface ResultAwareDynamicJobFn extends TaggedDynamicJobFn {
221
+ readonly [DYNAMIC_JOB_NEEDS_TAG]: ReadonlyArray<DynamicJobNeed>;
222
+ }
223
+ /**
224
+ * Read the declared needs from a result-aware generator.
225
+ * Returns undefined for event-only generators (the function form of dynamicJob).
226
+ */
227
+ export declare function getDynamicJobNeeds(fn: DynamicJobFn): ReadonlyArray<DynamicJobNeed> | undefined;
228
+ export { DYNAMIC_JOB_NEEDS_TAG };
180
229
  /**
181
230
  * Container configuration for job execution.
182
231
  * When set, all steps run inside the specified container.
@@ -254,8 +303,8 @@ export type InitConfig = InitItem | InitItem[] | 'auto' | false;
254
303
  * the agent's self-reported platform facts.
255
304
  */
256
305
  export interface RunsOnSelector {
257
- labels: string | string[];
258
- exclude?: string | string[];
306
+ labels: string | RegExp | (string | RegExp)[];
307
+ exclude?: string | RegExp | (string | RegExp)[];
259
308
  }
260
309
  /**
261
310
  * Polymorphic runsOn type: string shorthand, array shorthand, or full selector object.
@@ -263,18 +312,33 @@ export interface RunsOnSelector {
263
312
  * - `['kici:os:linux', 'gpu']` — multi-label shorthand (all must match)
264
313
  * - `{ labels: ['kici:os:linux'], exclude: ['kici:host:box-01'] }` — full selector
265
314
  *
315
+ * A plain string is matched exactly; a string containing glob metachars (`*?[]{}`) is a
316
+ * glob; a `RegExp` is a regex. Patterns are validated for ReDoS at compile time.
317
+ *
266
318
  * **Targeting `kici:` labels:** `runsOn` may target any agent label, including the
267
319
  * `kici:`-namespaced auto-labels (`kici:os:`, `kici:arch:`, `kici:host:`, `kici:agent:`,
268
320
  * `kici:scaler:`, `kici:role:`). Targeting is a requirement on candidate agents, never a
269
321
  * grant. `kici:scaler:` / `kici:agent:` are deployment-specific, so prefer custom labels
270
322
  * for portable pool targeting. Users still cannot *set* `kici:` labels on agents.
271
323
  */
272
- export type RunsOn = string | string[] | RunsOnSelector;
324
+ export type RunsOn = string | RegExp | (string | RegExp)[] | RunsOnSelector;
273
325
  /** Job definition returned by job() factory */
274
326
  export interface Job {
275
327
  readonly _tag: 'Job';
276
328
  readonly name: string;
277
- readonly runsOn: RunsOn;
329
+ /** Single-agent targeting. Mutually exclusive with `runsOnAll`. */
330
+ readonly runsOn?: RunsOn;
331
+ /**
332
+ * Host fan-out: run one pinned execution per roster host matching the predicate.
333
+ * Mutually exclusive with `runsOn`.
334
+ */
335
+ readonly runsOnAll?: RunsOnAllInput;
336
+ /** Failure policy for unreachable durable hosts when using `runsOnAll`. */
337
+ readonly onUnreachable?: OnUnreachableMode;
338
+ /** Fan-out concurrency width (sliding window; `1` = serial). Applies to matrix and `runsOnAll`. */
339
+ readonly maxParallel?: number;
340
+ /** Halt the fan-out on first child failure, skipping the remainder. Default `false`. */
341
+ readonly failFast?: boolean;
278
342
  readonly steps: readonly StepInput[];
279
343
  readonly needs?: ReadonlyArray<Job | string | DynamicGroupRef | {
280
344
  name: string;
@@ -355,7 +419,36 @@ export interface Job {
355
419
  }
356
420
  /** Options for job() factory */
357
421
  export interface JobOptions {
358
- runsOn: RunsOn;
422
+ /** Single-agent targeting. Mutually exclusive with `runsOnAll`. */
423
+ runsOn?: RunsOn;
424
+ /**
425
+ * Host fan-out: run one pinned execution per roster host matching the predicate.
426
+ * The author writes a label string (`'role:web'`), an array with `!`-prefixed
427
+ * excludes (`['kici:os:linux', 'role:db', '!kici:host:db-01']`), or the
428
+ * structured `{ include: [{ all: [...] }], exclude?: [...] }` form.
429
+ * Mutually exclusive with `runsOn`.
430
+ */
431
+ runsOnAll?: RunsOnAllInput;
432
+ /**
433
+ * Failure policy for unreachable durable hosts when using `runsOnAll`:
434
+ * `'skip'` omits them, `'fail'` fails the run, `'hold'` (default) queues a
435
+ * pinned child and waits. Only meaningful alongside `runsOnAll`.
436
+ */
437
+ onUnreachable?: OnUnreachableMode;
438
+ /**
439
+ * Fan-out concurrency width: the maximum number of fan-out children (matrix
440
+ * combinations or `runsOnAll` hosts) that run at once. A sliding window —
441
+ * each child that reaches a terminal state releases the next held sibling.
442
+ * `1` = strictly serial (rolling). Applies to both matrix and `runsOnAll`
443
+ * fan-out; ignored on a non-fan-out job. Must be `>= 1`.
444
+ */
445
+ maxParallel?: number;
446
+ /**
447
+ * Halt the fan-out on the first child failure: stop releasing new children
448
+ * and skip the ones still held. Default `false` (every child runs regardless
449
+ * of sibling outcomes). Applies to both matrix and `runsOnAll` fan-out.
450
+ */
451
+ failFast?: boolean;
359
452
  /**
360
453
  * Steps to execute in this job. Accepts Step objects and bare async functions.
361
454
  * Mutually exclusive with `run`.
package/dist/types.js CHANGED
@@ -11,15 +11,27 @@ const DYNAMIC_JOB_GROUP_TAG = Symbol.for("kici:dynamicJobGroup");
11
11
  * Tag a DynamicJobFn with a group name for cross-domain needs.
12
12
  * Static jobs can then depend on this group via `needs: [dynamicGroup('name')]`.
13
13
  *
14
+ * Two forms:
15
+ * - **Function form** (event-only): `dynamicJob('shards', async ({ ctx }) => [...])`.
16
+ * Dispatched at webhook time; deterministic from `ctx.event` alone.
17
+ * - **Options form** (result-aware): `dynamicJob('reports', { needs, generate })`.
18
+ * Deferred until every job/group in `needs` completes, then `generate` is
19
+ * evaluated with the upstreams' frozen outputs available as `ctx.needs`.
20
+ *
14
21
  * @param groupName - The group name (must match what static jobs reference)
15
- * @param fn - The dynamic job generator function
22
+ * @param fnOrConfig - The generator function, or a result-aware `{ needs, generate }` config
16
23
  */
17
- function dynamicJob(groupName, fn) {
18
- const tagged = fn;
24
+ function dynamicJob(groupName, fnOrConfig) {
25
+ const isConfig = typeof fnOrConfig !== "function";
26
+ const tagged = isConfig ? fnOrConfig.generate : fnOrConfig;
19
27
  Object.defineProperty(tagged, DYNAMIC_JOB_GROUP_TAG, {
20
28
  value: groupName,
21
29
  enumerable: false
22
30
  });
31
+ if (isConfig) Object.defineProperty(tagged, DYNAMIC_JOB_NEEDS_TAG, {
32
+ value: fnOrConfig.needs,
33
+ enumerable: false
34
+ });
23
35
  return tagged;
24
36
  }
25
37
  /**
@@ -29,7 +41,15 @@ function dynamicJob(groupName, fn) {
29
41
  function getDynamicJobGroup(fn) {
30
42
  return fn[DYNAMIC_JOB_GROUP_TAG];
31
43
  }
44
+ const DYNAMIC_JOB_NEEDS_TAG = Symbol.for("kici:dynamicJobNeeds");
45
+ /**
46
+ * Read the declared needs from a result-aware generator.
47
+ * Returns undefined for event-only generators (the function form of dynamicJob).
48
+ */
49
+ function getDynamicJobNeeds(fn) {
50
+ return fn[DYNAMIC_JOB_NEEDS_TAG];
51
+ }
32
52
  //#endregion
33
- export { DYNAMIC_JOB_GROUP_TAG, dynamicJob, getDynamicJobGroup, isDynamicJobFn };
53
+ export { DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, dynamicJob, getDynamicJobGroup, getDynamicJobNeeds, isDynamicJobFn };
34
54
 
35
55
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/sdk",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "TypeScript SDK for defining KiCI workflows. Import into `.kici/workflows/*.ts` to declare workflows, jobs, steps, triggers, rules, and matrix configurations.",
5
5
  "keywords": [
6
6
  "ci",
@@ -47,10 +47,10 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "micromatch": "^4.0.8",
50
- "zod": "^4.3.6",
50
+ "zod": "^4.4.3",
51
51
  "zx": "^8.8.5",
52
- "@kici-dev/core": "0.1.17",
53
- "@kici-dev/engine": "0.1.17"
52
+ "@kici-dev/core": "0.1.19",
53
+ "@kici-dev/engine": "0.1.19"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/micromatch": "^4.0.10"