@kici-dev/agent 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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Agent-process interception of the bootstrap bring-up API methods.
3
+ *
4
+ * `ctx.kici.bootstrap.ensureInitRunner` / `preBootSend` are relayed from the
5
+ * workflow sandbox as `agent.api.request` IPC. The agent process intercepts
6
+ * those two methods HERE (rather than relaying them straight to the
7
+ * orchestrator) so the SSH transport — and the bring-up key / bootstrap token
8
+ * the orchestrator hands back — stay in the agent process, never reaching user
9
+ * workflow code. Every other method falls through to the orchestrator relay
10
+ * unchanged.
11
+ */
12
+ import { type ApiTransport, type EnsureInitRunnerDeps } from './ensure-init-runner.js';
13
+ /**
14
+ * Wrap the orchestrator API transport so the two bootstrap methods are handled
15
+ * in-process (SSH transport here; privileged resolve relayed to the
16
+ * orchestrator). `relay` is the raw orchestrator transport (the WS
17
+ * `sendApiRequest`).
18
+ */
19
+ export declare function withBootstrapInterception(relay: ApiTransport, deps?: EnsureInitRunnerDeps): (method: string, params?: Record<string, unknown>) => Promise<unknown>;
20
+ //# sourceMappingURL=api-intercept.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Agent-side init-runner bring-up.
3
+ *
4
+ * Runs in the AGENT process (never the workflow sandbox), so the bring-up SSH
5
+ * key and bootstrap token the orchestrator hands back never reach user
6
+ * workflow code. The flow:
7
+ *
8
+ * 1. Call the orchestrator's privileged `kici.ensureInitRunner` handler — it
9
+ * gates on this agent's `kici:capability:ssh-transport` capability,
10
+ * resolves the target's reach + SSH key, mints a single-use bootstrap
11
+ * token, audits, and returns the material (or `{ broughtUp: false }` when
12
+ * the target already has a live agent).
13
+ * 2. Over SSH (ephemeral key, never on disk): drop a launcher onto the target
14
+ * that starts `kici-agent` with the bootstrap env (token + agent id +
15
+ * orchestrator URL + labels), and start it detached.
16
+ *
17
+ * The init-runner then connects → `auth.request` (bootstrap token) →
18
+ * `agent.register` auto-enroll as a temporary `kici:init` agent.
19
+ */
20
+ import { type SshDeps } from './ssh-exec.js';
21
+ /** Transport that relays an API request to the orchestrator and awaits the result. */
22
+ export type ApiTransport = (method: string, params: Record<string, unknown>) => Promise<unknown>;
23
+ export interface EnsureInitRunnerDeps extends SshDeps {
24
+ /**
25
+ * The command used to start the init-runner on the target. Defaults to
26
+ * `kici-agent` (resolved on the target's PATH). Override for a rescue env
27
+ * that stages the binary at a fixed path.
28
+ */
29
+ agentCommand?: string;
30
+ }
31
+ /**
32
+ * Bring up a temporary init-runner on `targetAgentId`. Returns `{ broughtUp }`:
33
+ * false when the target already had a live agent (the orchestrator no-op'd),
34
+ * true when this call dropped + started the init-runner.
35
+ */
36
+ export declare function ensureInitRunner(transport: ApiTransport, targetAgentId: string, deps?: EnsureInitRunnerDeps): Promise<{
37
+ broughtUp: boolean;
38
+ }>;
39
+ //# sourceMappingURL=ensure-init-runner.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Agent-side pre-boot input send (dropbear / initramfs unlock).
3
+ *
4
+ * Runs in the AGENT process. Asks the orchestrator's privileged
5
+ * `kici.preBootSend` handler to gate on the `kici:capability:ssh-transport`
6
+ * capability, resolve the input secret (e.g. a LUKS passphrase), and audit;
7
+ * then pipes the resolved input to the target's pre-boot SSH endpoint
8
+ * (default dropbear port 2222, forced `cryptroot-unlock`). The unlock drops
9
+ * the session as the box boots — success is the send completing, not the SSH
10
+ * exit code; the caller composes a host-alive wait to confirm boot.
11
+ */
12
+ import { type SshDeps } from './ssh-exec.js';
13
+ import type { ApiTransport } from './ensure-init-runner.js';
14
+ export interface PreBootSendOpts {
15
+ inputSecret: string;
16
+ port?: number;
17
+ command?: string;
18
+ }
19
+ /**
20
+ * Ship a pre-boot input to the target's dropbear/initramfs SSH channel. The
21
+ * input plaintext is resolved server-side and never logged. Resolves once the
22
+ * send completes (the SSH session legitimately drops as the box boots).
23
+ */
24
+ export declare function preBootSend(transport: ApiTransport, targetAgentId: string, opts: PreBootSendOpts, deps?: SshDeps): Promise<void>;
25
+ //# sourceMappingURL=pre-boot-send.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Agent-facing host reach metadata: how to SSH to a target for bootstrap.
3
+ *
4
+ * This is the subset the agent's SSH helper needs — the connection
5
+ * coordinates only. It deliberately does NOT carry the `sshKeySecret` ref:
6
+ * the orchestrator resolves the scoped secret server-side and hands the agent
7
+ * the already-resolved private key separately, so the secret ref never reaches
8
+ * the agent. `sshPort` is the OS-sshd port for bring-up; a pre-boot dropbear
9
+ * port is supplied per-call via `SshExecOpts.port`.
10
+ */
11
+ export interface HostReach {
12
+ agentId: string;
13
+ address: string | null;
14
+ sshUser: string | null;
15
+ sshPort: number | null;
16
+ }
17
+ //# sourceMappingURL=reach.d.ts.map
@@ -0,0 +1,58 @@
1
+ import type { HostReach } from './reach.js';
2
+ /** Host-key verification mode for the SSH connection. */
3
+ export type SshHostKeyMode = 'accept-new' | 'strict';
4
+ /** Result of an SSH invocation: exit code is reported, never thrown away. */
5
+ export interface SshResult {
6
+ exitCode: number;
7
+ stdout: string;
8
+ stderr: string;
9
+ }
10
+ export interface SshExecOpts {
11
+ /** Piped to the remote command's stdin (e.g. a LUKS passphrase to cryptroot-unlock). */
12
+ stdin?: string;
13
+ /** Override the default SSH port (22). E.g. 2222 for a dropbear initramfs prompt. */
14
+ port?: number;
15
+ /**
16
+ * Host-key verification. Defaults to `accept-new` — the dropbear initramfs
17
+ * host key differs from the OS sshd key, so a pinned OS entry must not be
18
+ * reused. `strict` enforces a known_hosts match.
19
+ */
20
+ hostKeyMode?: SshHostKeyMode;
21
+ }
22
+ /**
23
+ * Low-level process spawn boundary, injectable so unit tests can assert the
24
+ * exact argv / env / stdin without running a real `ssh`. Mirrors the relevant
25
+ * subset of `child_process.spawn`'s contract.
26
+ */
27
+ export interface SpawnFn {
28
+ (command: string, args: string[], opts: {
29
+ env: NodeJS.ProcessEnv;
30
+ stdin?: string;
31
+ }): Promise<SshResult>;
32
+ }
33
+ /** Default spawn boundary backed by `node:child_process.spawn`. */
34
+ export declare const defaultSpawn: SpawnFn;
35
+ /** Deps for the SSH helper — the spawn boundary is injectable for tests. */
36
+ export interface SshDeps {
37
+ spawnFn?: SpawnFn;
38
+ }
39
+ /**
40
+ * Run a command on the target over SSH using an ephemeral, in-memory key.
41
+ *
42
+ * The key is loaded into a per-call ssh-agent (never written to disk) and the
43
+ * agent is killed in `finally`. The remote command's exit code / stdout /
44
+ * stderr are returned verbatim — a non-zero exit is reported, not swallowed
45
+ * (a pre-boot unlock legitimately drops the session, so the caller decides
46
+ * what a "success" looks like).
47
+ */
48
+ export declare function sshExec(reach: HostReach, privateKey: string, command: string, opts?: SshExecOpts, deps?: SshDeps): Promise<SshResult>;
49
+ /**
50
+ * Ship local bytes to a remote path over SSH (`ssh 'cat > path'`), using the
51
+ * same ephemeral-key discipline. Throws on a non-zero exit (a push must
52
+ * succeed end-to-end, unlike a pre-boot unlock).
53
+ */
54
+ export declare function sshPush(reach: HostReach, privateKey: string, localBytes: string, remotePath: string, opts?: {
55
+ port?: number;
56
+ hostKeyMode?: SshHostKeyMode;
57
+ }, deps?: SshDeps): Promise<void>;
58
+ //# sourceMappingURL=ssh-exec.d.ts.map
@@ -10,20 +10,37 @@ export interface CachePhaseDeps {
10
10
  cache: CacheApi;
11
11
  /** Emit a runner→agent IPC message (the masked send). */
12
12
  sendIpc: (msg: RunnerToAgentMessage) => void;
13
- /** Monotonic pseudo-step index allocator (continues after real steps + hooks). */
14
- nextStepIndex: () => number;
13
+ /**
14
+ * Allocate the next cache pseudo-step index for `ownerStepIndex` (the real
15
+ * step the cache op belongs to, or {@link JOB_CACHE_OWNER} for job-level
16
+ * cache). Each owner draws from a disjoint block above every real-step and
17
+ * hook index, so two concurrently-running steps' cache pseudo-steps never
18
+ * collide.
19
+ */
20
+ nextStepIndex: (ownerStepIndex: number) => number;
15
21
  }
22
+ /** Owner sentinel for job-level (not step-scoped) cache restore/save. */
23
+ export declare const JOB_CACHE_OWNER = -1;
24
+ /**
25
+ * Build the cache pseudo-step index allocator. Each owner (a real step index, or
26
+ * {@link JOB_CACHE_OWNER}) gets its own disjoint block of {@link CACHE_INDEX_BLOCK}
27
+ * indices, all above every real-step and hook index (`stepCount * 3 + 100`). A
28
+ * step's two-or-more cache pseudo-steps are a pure function of its own owner
29
+ * index, so concurrent children never collide. Under sequential execution the
30
+ * emitted indices stay above all real/hook indices exactly as before.
31
+ */
32
+ export declare function createCacheStepIndexAllocator(stepCount: number): (ownerStepIndex: number) => number;
16
33
  /**
17
34
  * Restore every spec, surfacing each as a `cache:restore` pseudo-step. Returns
18
35
  * a map keyed by spec key recording whether the EXACT key hit (so the save
19
36
  * phase can skip a redundant save of an entry that already exists).
20
37
  */
21
- export declare function restoreCacheSpecs(specs: CacheSpec[], deps: CachePhaseDeps): Promise<Map<string, CacheRestoreOutcome>>;
38
+ export declare function restoreCacheSpecs(specs: CacheSpec[], deps: CachePhaseDeps, ownerStepIndex: number): Promise<Map<string, CacheRestoreOutcome>>;
22
39
  /**
23
40
  * Save every spec whose EXACT key did not already hit on restore (immutable +
24
41
  * no redundant save), surfacing each as a `cache:save` pseudo-step. A spec
25
42
  * whose restore matched a different key via a `restoreKeys` prefix is still
26
43
  * saved under its exact key.
27
44
  */
28
- export declare function saveCacheSpecs(specs: CacheSpec[], restoreResults: Map<string, CacheRestoreOutcome>, deps: CachePhaseDeps): Promise<void>;
45
+ export declare function saveCacheSpecs(specs: CacheSpec[], restoreResults: Map<string, CacheRestoreOutcome>, deps: CachePhaseDeps, ownerStepIndex: number): Promise<void>;
29
46
  //# sourceMappingURL=cache-phase.d.ts.map
@@ -5,5 +5,5 @@
5
5
  * `ctx.cache` API factory plus its transport interface.
6
6
  */
7
7
  export { createCacheApi, packCachePaths, extractCacheTarball, downloadAndExtractCache, resolveCachePath, type CacheTransport, type CacheRoots, } from './cache-engine.js';
8
- export { restoreCacheSpecs, saveCacheSpecs, type CachePhaseDeps, type CacheRestoreOutcome, } from './cache-phase.js';
8
+ export { restoreCacheSpecs, saveCacheSpecs, createCacheStepIndexAllocator, JOB_CACHE_OWNER, type CachePhaseDeps, type CacheRestoreOutcome, } from './cache-phase.js';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,7 +5,8 @@ import { type MatrixValues } from '@kici-dev/engine';
5
5
  * Only fields that were flagged as dynamic and successfully resolved are set.
6
6
  */
7
7
  export interface InitResult {
8
- environmentName?: string;
8
+ /** Resolved bound-environment names, in merge order (one per `environments` element). */
9
+ environmentNames?: string[];
9
10
  env?: Record<string, string>;
10
11
  concurrencyGroup?: string;
11
12
  /**
@@ -215,6 +215,18 @@ export declare class JobRunner {
215
215
  * failure diagnostics, and send the terminal `job.status` message.
216
216
  */
217
217
  private reportExecutionResult;
218
+ /**
219
+ * Handle a bring-up job: bring up a temporary init-runner on a declared-but-
220
+ * un-agented host over SSH (fresh-box bootstrap convergence). The orchestrator
221
+ * dispatches this synthetic `__bringup__` job to an agent holding the
222
+ * `kici:capability:ssh-transport` capability; here we run the agent-side
223
+ * `ensureInitRunner` helper (the privileged resolve is relayed to the
224
+ * orchestrator, the SSH transport happens in this agent process — never
225
+ * reaching workflow code). No clone, no sandbox — the init-runner then
226
+ * connects under the target's agent id and the orchestrator's pinned-hold
227
+ * drains the target's bootstrap steps onto it.
228
+ */
229
+ private handleBringupJob;
218
230
  /**
219
231
  * Handle a build-only job.
220
232
  *
@@ -1,11 +1,13 @@
1
- import type { RuleContext } from '@kici-dev/sdk';
1
+ import type { RuleContext, FanoutPosition } from '@kici-dev/sdk';
2
2
  /**
3
3
  * Create RuleContext for agent-side rule evaluation.
4
4
  *
5
5
  * @param event - Event payload from the dispatch message
6
6
  * @param changedFiles - List of files changed in this event
7
7
  * @param env - Merged environment variables
8
+ * @param dispatchInputs - Operator dispatch inputs (`ctx.dispatchInputs`)
9
+ * @param fanout - Fan-out position (`ctx.fanout`); undefined on a non-fan-out job
8
10
  */
9
- export declare function createRuleContext(event: Record<string, unknown>, changedFiles?: string[], env?: Record<string, string | undefined>): RuleContext;
11
+ export declare function createRuleContext(event: Record<string, unknown>, changedFiles?: string[], env?: Record<string, string | undefined>, dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>, fanout?: FanoutPosition): RuleContext;
10
12
  export { evaluateRules, type RuleEvaluationResult } from '@kici-dev/sdk';
11
13
  //# sourceMappingURL=rule-evaluator.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Run `fn` with `stepIndex` as the active console-capture attribution for the
3
+ * duration of its async execution (including everything it awaits).
4
+ */
5
+ export declare function runInStepCapture<T>(stepIndex: number, fn: () => Promise<T>): Promise<T>;
6
+ /**
7
+ * The step index whose run is currently on the async stack, or `-1` when no
8
+ * capture scope is active (workflow-level / between-steps output).
9
+ */
10
+ export declare function currentCaptureStepIndex(): number;
11
+ //# sourceMappingURL=capture-context.d.ts.map
@@ -24,13 +24,27 @@ interface StepStartMessage {
24
24
  stepName: string;
25
25
  /** Distinguishes regular steps from hook executions (e.g., 'hook:onCancel', 'hook:cleanup'). Defaults to 'step'. */
26
26
  step_type?: string;
27
+ /**
28
+ * Initial state for the step. Defaults to `running`. A parallel-group child
29
+ * queued behind `maxParallel` is announced as `pending` before it acquires a
30
+ * slot; it later emits a second `step.start` with `running` when it launches.
31
+ */
32
+ state?: 'running' | 'pending';
33
+ /** Step concurrency role; absent means an ordinary sequential step. */
34
+ concurrencyKind?: string;
35
+ /** Parallel-group correlation id shared by a group's children (e.g. `g0`). */
36
+ groupId?: string;
27
37
  }
28
- /** A step has completed (success, failure, or a check-mode skip). */
38
+ /** A step has completed (success, failure, check-mode skip, or fail-fast cancel). */
29
39
  interface StepCompleteMessage {
30
40
  type: 'step.complete';
31
41
  stepIndex: number;
32
- status: 'success' | 'failed' | 'skipped';
42
+ status: 'success' | 'failed' | 'skipped' | 'cancelled';
33
43
  durationMs: number;
44
+ /** Step concurrency role; absent means an ordinary sequential step. */
45
+ concurrencyKind?: string;
46
+ /** Parallel-group correlation id shared by a group's children (e.g. `g0`). */
47
+ groupId?: string;
34
48
  error?: {
35
49
  message: string;
36
50
  exitCode?: number;
@@ -410,6 +424,20 @@ export interface JobExecutionRequest {
410
424
  platform?: string;
411
425
  arch?: string;
412
426
  };
427
+ /**
428
+ * Operator-supplied, validated + coerced + defaulted workflow-dispatch inputs
429
+ * (from `kici run --input`), exposed to steps + rules as `ctx.dispatchInputs`.
430
+ * Absent for webhook runs.
431
+ */
432
+ dispatchInputs?: Record<string, unknown>;
433
+ /**
434
+ * For a fan-out child (`runsOnAll` host or matrix combination): the 0-based
435
+ * deterministic position in the fan-out, assembled into `ctx.fanout`. Absent
436
+ * for non-fan-out jobs.
437
+ */
438
+ fanoutIndex?: number;
439
+ /** For a fan-out child: the number of children in this fan-out. */
440
+ fanoutTotal?: number;
413
441
  /** Secrets to merge into step environment (highest precedence). */
414
442
  secrets?: Record<string, string>;
415
443
  /** Namespaced secrets by context name for ctx.secrets['context-name'].KEY access. */
@@ -440,7 +468,7 @@ export interface JobExecutionRequest {
440
468
  provider?: string;
441
469
  /** Whether to checkout the repo (default: true). */
442
470
  checkout?: boolean;
443
- /** Whether this job is part of a test run triggered by `kici test`. */
471
+ /** Whether this job is part of a developer-initiated run triggered by `kici run`. */
444
472
  isTestRun?: boolean;
445
473
  /**
446
474
  * Run mode for idempotent steps (`apply` | `check` | `check-fail-on-drift`).
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Concurrency-aware scheduler for `parallel()` step groups.
3
+ *
4
+ * A parallel group's children each run as their own observable step (own logs,
5
+ * status, timing, retry, cache, hooks — all task-scoped by the Phase 0 per-task
6
+ * isolation) through the same `runStepIteration` machinery the sequential loop
7
+ * uses. Children launch behind a `maxParallel` window (queued children report
8
+ * `pending`); the group joins at a barrier. On the first non-`continueOnError`
9
+ * child failure when `failFast`, every in-flight sibling's per-task abort
10
+ * controller is fired so its step race rejects and it is reported `cancelled`
11
+ * (which is NOT a failure).
12
+ */
13
+ import { type StepLoopOptions, type StepNode } from './step-loop.js';
14
+ import type { SandboxStepResult } from './types.js';
15
+ type ParallelNode = Extract<StepNode, {
16
+ kind: 'parallel';
17
+ }>;
18
+ /** Outcome of running one parallel group. */
19
+ export interface ParallelGroupOutcome {
20
+ /** True when at least one non-`continueOnError` child failed. */
21
+ failed: boolean;
22
+ /** Name of the first failing child (drives the job failure reason). */
23
+ failedStepName?: string;
24
+ /** Per-child results, in child array order. */
25
+ results: SandboxStepResult[];
26
+ }
27
+ /**
28
+ * Run a parallel group: launch children with a bounded-concurrency window, join
29
+ * at a barrier, and fail-fast-cancel in-flight siblings on the first hard
30
+ * failure.
31
+ */
32
+ export declare function runParallelGroup(node: ParallelNode, opts: StepLoopOptions): Promise<ParallelGroupOutcome>;
33
+ export {};
34
+ //# sourceMappingURL=parallel-scheduler.d.ts.map
@@ -5,11 +5,41 @@
5
5
  * Extracted for testability: the workflow-runner's main() handles IPC, clone, deps,
6
6
  * module loading, and calls this loop for step execution with hooks.
7
7
  */
8
- import type { Step, StepContext, HookInput, OutputsMap, StepSecretMountRecord } from '@kici-dev/sdk';
8
+ import type { Step, StepContext, HookInput, OutputsMap, StepSecretMountRecord, FanoutPosition } from '@kici-dev/sdk';
9
9
  import { CheckMode } from '@kici-dev/engine';
10
10
  import type { RunnerToAgentMessage } from './ipc-protocol.js';
11
11
  import type { SandboxStepResult } from './types.js';
12
12
  import { type CachePhaseDeps } from '../cache/index.js';
13
+ /**
14
+ * Thrown inside the step race when a step's own per-task abort controller fires
15
+ * (parallel fail-fast cancels an in-flight sibling). Distinguished from a
16
+ * timeout/job-deadline reject so the loop reports the step as `cancelled`
17
+ * (which is NOT a failure) rather than `failed`.
18
+ */
19
+ export declare class StepCancelledError extends Error {
20
+ readonly name = "StepCancelledError";
21
+ constructor(stepName: string);
22
+ }
23
+ /**
24
+ * A node in the concurrency-aware step walk. A `sequential` node is one ordinary
25
+ * step; a `parallel` node is a `parallel()` group whose children each carry their
26
+ * own flat `stepIndex` (the group wrapper consumes no index).
27
+ */
28
+ export type StepNode = {
29
+ kind: 'sequential';
30
+ step: Step;
31
+ stepIndex: number;
32
+ } | {
33
+ kind: 'parallel';
34
+ groupId: string;
35
+ name: string;
36
+ failFast: boolean;
37
+ maxParallel?: number;
38
+ children: {
39
+ step: Step;
40
+ stepIndex: number;
41
+ }[];
42
+ };
13
43
  /** Job-level hooks passed to the step loop. */
14
44
  export interface JobHooks {
15
45
  beforeStep?: HookInput;
@@ -21,7 +51,32 @@ export interface JobHooks {
21
51
  }
22
52
  /** Options for the step execution loop. */
23
53
  export interface StepLoopOptions {
54
+ /**
55
+ * Flat list of every executable step (sequential steps + parallel-group
56
+ * children inlined in flat-stepIndex order). `steps.length` is the flat step
57
+ * count used to derive hook pseudo-indices. The structural walk order (which
58
+ * entries are grouped) is carried separately by `stepNodes`.
59
+ */
24
60
  steps: Step[];
61
+ /**
62
+ * Structural walk order: sequential steps and parallel groups in array order.
63
+ * When present, the loop walks these nodes (dispatching parallel groups to the
64
+ * concurrency-aware scheduler); when absent, it walks `steps` sequentially with
65
+ * the array index as the stepIndex (unit-harness back-compat).
66
+ */
67
+ stepNodes?: StepNode[];
68
+ /**
69
+ * Abort the per-task controller for `stepIndex` (parallel fail-fast). Wired to
70
+ * the workflow-runner's `stepAbortControllers` map so an aborted sibling's
71
+ * `ctx.signal` fires and its step race rejects with {@link StepCancelledError}.
72
+ */
73
+ abortStep?: (stepIndex: number) => void;
74
+ /**
75
+ * Returns the per-task abort signal for `stepIndex` (the same controller
76
+ * `abortStep` triggers). The step race watches it so a fail-fast abort
77
+ * interrupts an in-flight step even if its body ignores `ctx.signal`.
78
+ */
79
+ getStepAbortSignal?: (stepIndex: number) => AbortSignal | undefined;
25
80
  /**
26
81
  * Run mode for idempotent steps. `apply` (default) converges; `check` /
27
82
  * `check-fail-on-drift` preview drift and never invoke a checked step's apply.
@@ -29,6 +84,12 @@ export interface StepLoopOptions {
29
84
  checkMode?: CheckMode;
30
85
  /** Factory that creates a StepContext for a given step index and name. */
31
86
  createStepContext: (stepIndex: number, stepName: string) => StepContext;
87
+ /**
88
+ * Run `fn` inside the step's console-capture scope so any console output it
89
+ * (or its hooks) produces attributes to `stepIndex`. Defaults to calling `fn`
90
+ * directly when absent (unit harnesses without capture wiring).
91
+ */
92
+ runWithStepCapture?: <T>(stepIndex: number, fn: () => Promise<T>) => Promise<T>;
32
93
  sendIpc: (msg: RunnerToAgentMessage) => void;
33
94
  defaultTimeoutMs: number;
34
95
  outputsMap: OutputsMap;
@@ -36,6 +97,10 @@ export interface StepLoopOptions {
36
97
  event: Record<string, unknown>;
37
98
  /** Environment variables for rule context. */
38
99
  env: Record<string, string | undefined>;
100
+ /** Operator dispatch inputs for the rule context (`ctx.dispatchInputs`). */
101
+ dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>;
102
+ /** Fan-out position for the rule context (`ctx.fanout`); undefined on a non-fan-out job. */
103
+ fanout?: FanoutPosition;
39
104
  /** Job-level hooks. */
40
105
  jobHooks?: JobHooks;
41
106
  /**
@@ -59,36 +124,38 @@ export interface StepLoopOptions {
59
124
  /** Job start time (epoch ms) for outcome metadata duration. */
60
125
  startTime?: number;
61
126
  /**
62
- * Returns the secret key names accessed by the most recently created step context.
63
- * Called after each step completes to include in step.complete IPC messages.
127
+ * Returns the secret key names accessed by the step context created for
128
+ * `stepIndex`. Called after each step completes to include in step.complete
129
+ * IPC messages.
64
130
  */
65
- getSecretsAccessLog?: () => string[];
131
+ getSecretsAccessLog?: (stepIndex: number) => string[];
66
132
  /**
67
- * Tear down per-step state created by the most recent `createStepContext`
68
- * call. Invoked from the step-loop's `finally` after the step completes
133
+ * Tear down per-step state created by the `createStepContext` call for
134
+ * `stepIndex`. Invoked from the step-loop's `finally` after the step completes
69
135
  * (success, failure, rule-skip, or timeout) so resources like the
70
136
  * `ctx.secrets.mountFile` tmpdir get removed even on the failure paths.
71
137
  * Never throws -- errors are logged by the wired implementation.
72
138
  */
73
- disposeStepResources?: () => Promise<void>;
139
+ disposeStepResources?: (stepIndex: number) => Promise<void>;
74
140
  /**
75
- * Returns the IPC `step.secret_mount` records collected by the most
76
- * recently created step context. Emitted on step completion so the
77
- * orchestrator can persist the audit trail alongside `secretsAccessed`.
141
+ * Returns the IPC `step.secret_mount` records collected by the step context
142
+ * created for `stepIndex`. Emitted on step completion so the orchestrator can
143
+ * persist the audit trail alongside `secretsAccessed`.
78
144
  */
79
- getSecretMountRecords?: () => StepSecretMountRecord[];
145
+ getSecretMountRecords?: (stepIndex: number) => StepSecretMountRecord[];
80
146
  /**
81
147
  * Before a step's run function executes, point KICI_ENV / KICI_PATH at fresh
82
- * temp files for this step. Invoked once per executed step (NOT for rule-skipped
148
+ * temp files for this step (keyed by `stepIndex` so concurrent steps never
149
+ * share a delta file). Invoked once per executed step (NOT for rule-skipped
83
150
  * steps). The workflow-runner owns the file lifecycle.
84
151
  */
85
- beforeStepEnvFiles?: () => Promise<void>;
152
+ beforeStepEnvFiles?: (stepIndex: number) => Promise<void>;
86
153
  /**
87
- * After a step's run function completes (success OR failure), read the
88
- * KICI_ENV / KICI_PATH files, apply the delta via applyEnvDelta, and truncate
89
- * them for the next step. Never throws -- errors are logged by the wired impl.
154
+ * After a step's run function completes (success OR failure), read this
155
+ * step's KICI_ENV / KICI_PATH files, apply the delta via applyEnvDelta, and
156
+ * release them. Never throws -- errors are logged by the wired impl.
90
157
  */
91
- afterStepApplyEnvFiles?: () => Promise<void>;
158
+ afterStepApplyEnvFiles?: (stepIndex: number) => Promise<void>;
92
159
  /**
93
160
  * Block an `approval` step (`when: 'always'`) pending an orchestrator-side
94
161
  * approval hold. The runner sends the normalized requirement and awaits the
@@ -141,6 +208,18 @@ interface StepLoopResult {
141
208
  stepResults: SandboxStepResult[];
142
209
  failureReason?: string;
143
210
  }
211
+ /**
212
+ * Per-step iteration outcome returned by `runStepIteration`.
213
+ */
214
+ export interface StepIterationOutcome {
215
+ /** The result to append to the running stepResults list. */
216
+ result: SandboxStepResult;
217
+ /** When true, the loop must break (failed step without continueOnError). */
218
+ shouldBreak: boolean;
219
+ /** Set when the step failed; carried into completion-hook outcome metadata. */
220
+ failedStepName?: string;
221
+ }
222
+ export declare function runStepIteration(step: Step, stepIndex: number, opts: StepLoopOptions): Promise<StepIterationOutcome>;
144
223
  /**
145
224
  * Execute the step loop with hook integration and step-level rule evaluation.
146
225
  *
@@ -0,0 +1,26 @@
1
+ import type { StepSecretMountRecord, TrackedStepSecrets } from '@kici-dev/sdk';
2
+ /** Per-step secrets handle + its teardown closure, keyed by step index. */
3
+ export interface StepTaskSlot {
4
+ secrets: TrackedStepSecrets;
5
+ dispose: () => Promise<void>;
6
+ }
7
+ /**
8
+ * Per-task replacement for the runner's former `currentStepSecrets` /
9
+ * `currentStepDispose` single-slots.
10
+ *
11
+ * The runner used to remember only the *most recent* step's secrets handle and
12
+ * dispose closure; the access-log / mount-record / dispose reader callbacks read
13
+ * that single slot. Under sequential execution that is correct (one step at a
14
+ * time), but two concurrently-running steps would clobber each other's
15
+ * secrets-audit trail. Keying every slot by the step's index keeps each step's
16
+ * audit trail and teardown isolated — sequential behavior is identical, Phase 1
17
+ * concurrency is correct.
18
+ */
19
+ export declare class StepTaskRegistry {
20
+ #private;
21
+ set(stepIndex: number, slot: StepTaskSlot): void;
22
+ getAccessLog(stepIndex: number): string[];
23
+ getMountRecords(stepIndex: number): StepSecretMountRecord[];
24
+ dispose(stepIndex: number): Promise<void>;
25
+ }
26
+ //# sourceMappingURL=step-task-registry.d.ts.map
@@ -150,8 +150,8 @@ export interface SandboxStepResult {
150
150
  name: string;
151
151
  /** Zero-based index of the step within the job. */
152
152
  stepIndex: number;
153
- /** Step execution status. */
154
- status: 'success' | 'failed' | 'skipped';
153
+ /** Step execution status. `cancelled` = a parallel sibling fail-fast cancel. */
154
+ status: 'success' | 'failed' | 'skipped' | 'cancelled';
155
155
  /** Step duration in milliseconds. */
156
156
  durationMs: number;
157
157
  /** Error details when status is 'failed'. */
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { ExecutionJobStatus } from '@kici-dev/engine';
17
17
  import type { StepContext } from '@kici-dev/sdk';
18
- import type { NeedsContext } from '@kici-dev/sdk';
18
+ import type { NeedsContext, FanoutPosition } from '@kici-dev/sdk';
19
19
  import type { OutputsMap, StepRefMap, TrackedStepSecrets } from '@kici-dev/sdk';
20
20
  import type { RunnerToAgentMessage, JobExecutionRequest } from './ipc-protocol.js';
21
21
  import { LogMasker } from './log-masker.js';
@@ -35,7 +35,34 @@ export declare function buildStepNeedsContext(declaredNeeds: readonly unknown[]
35
35
  * NOT serialized across the process boundary. This means zx $ runs natively
36
36
  * inside this process with full shell access.
37
37
  */
38
- export declare function createSandboxStepContext(workDir: string, stepIndex: number, stepName: string, request: JobExecutionRequest, maskedSendFn: (msg: RunnerToAgentMessage) => void, outputsMap: OutputsMap, refMap: StepRefMap, operatorSecretKeys: Set<string>, secretOutputs: Map<string, string>, jobOutputsMap: OutputsMap, secrets: TrackedStepSecrets, masker: LogMasker): StepContext;
38
+ export declare function createSandboxStepContext(workDir: string, stepIndex: number, stepName: string, request: JobExecutionRequest, maskedSendFn: (msg: RunnerToAgentMessage) => void, outputsMap: OutputsMap, refMap: StepRefMap, operatorSecretKeys: Set<string>, secretOutputs: Map<string, string>, jobOutputsMap: OutputsMap, secrets: TrackedStepSecrets, masker: LogMasker, signal: AbortSignal): StepContext;
39
+ /**
40
+ * Derive the fan-out position (`ctx.fanout`) from a dispatch request. Returns
41
+ * `undefined` for a non-fan-out job (no `fanoutTotal`), so `ctx.fanout` is only
42
+ * set for `runsOnAll` host children and matrix combinations.
43
+ */
44
+ export declare function deriveFanout(request: JobExecutionRequest): FanoutPosition | undefined;
39
45
  /** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
40
46
  export declare function rawPayloadFromEvent(event: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
47
+ /**
48
+ * Build the step loop's KICI_ENV/KICI_PATH callbacks with a per-step delta-file
49
+ * pair (keyed by step index). `beforeStepEnvFiles(stepIndex)` lazily creates the
50
+ * step's pair and points the runner's process.env at it (each step's zx $
51
+ * snapshots process.env at context creation, which happens AFTER this
52
+ * before-hook, so the shell sees them; the pre-fork env allowlist does not
53
+ * re-filter runtime-set vars). `afterStepApplyEnvFiles(stepIndex)` applies that
54
+ * step's delta and releases the pair.
55
+ *
56
+ * Env-isolation contract: under sequential execution this is identical to a
57
+ * single shared pair truncated between steps — each step still sees only its own
58
+ * delta. The pair is now per-step so two concurrently-running steps cannot
59
+ * corrupt each other's delta file. `process.env.KICI_ENV` / `process.env.KICI_PATH`
60
+ * remain process-global, so Phase 1 forbids `setEnv` / `addPath` / `$KICI_ENV`
61
+ * writes inside `parallel()` children (compile-time validation); Phase 0 only
62
+ * makes the file pair per-task.
63
+ */
64
+ export declare function buildStepEnvFileHooks(operatorSecretKeys: Set<string>, maskedSend: (msg: RunnerToAgentMessage) => void): {
65
+ beforeStepEnvFiles: (stepIndex: number) => Promise<void>;
66
+ afterStepApplyEnvFiles: (stepIndex: number) => Promise<void>;
67
+ };
41
68
  //# sourceMappingURL=workflow-runner.d.ts.map