@kici-dev/compiler 0.4.0 → 0.5.0

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 (36) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/commands/compile.js +1 -13
  3. package/dist/commands/preview.js +1 -8
  4. package/dist/commands/types.js +1 -2
  5. package/dist/errors/formatter.d.ts +2 -4
  6. package/dist/errors/formatter.js +1 -3
  7. package/dist/errors/index.d.ts +1 -1
  8. package/dist/errors/index.js +2 -2
  9. package/dist/generators/secrets-dts.d.ts +0 -1
  10. package/dist/generators/secrets-dts.js +1 -2
  11. package/dist/llm-context/llms-architecture.txt +27 -13
  12. package/dist/llm-context/llms-cli.txt +26 -6
  13. package/dist/llm-context/llms-features.txt +272 -91
  14. package/dist/llm-context/llms-full.txt +649 -235
  15. package/dist/llm-context/llms-getting-started.txt +20 -20
  16. package/dist/llm-context/llms-patterns.txt +10 -6
  17. package/dist/llm-context/llms-providers.txt +4 -6
  18. package/dist/llm-context/llms-sdk-runtime.txt +37 -36
  19. package/dist/llm-context/llms-sdk.txt +253 -57
  20. package/dist/llm-context/llms.txt +6 -6
  21. package/dist/local-plane/plane-manager.js +2 -2
  22. package/dist/lockfile/generator.js +137 -42
  23. package/dist/lockfile/index.d.ts +0 -2
  24. package/dist/lockfile/index.js +1 -2
  25. package/dist/templates/package-json.js +1 -1
  26. package/dist/test-runner/dry-run.d.ts +1 -2
  27. package/dist/test-runner/dry-run.js +1 -18
  28. package/dist/types.d.ts +32 -8
  29. package/dist/types.js +7 -1
  30. package/dist/validation/validator.js +40 -0
  31. package/package.json +6 -6
  32. package/sbom.spdx.json +126 -121
  33. package/dist/lockfile/purity-analyzer.d.ts +0 -25
  34. package/dist/lockfile/purity-analyzer.js +0 -204
  35. package/dist/lockfile/purity-diagnostics.d.ts +0 -31
  36. package/dist/lockfile/purity-diagnostics.js +0 -52
@@ -36,19 +36,19 @@ Your workflow is plain TypeScript, but different parts of it run at three distin
36
36
 
37
37
  ## The three phases
38
38
 
39
- | Phase | Where it runs | What runs | When |
40
- | ---------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
41
- | **Compile** | Your dev machine or CI (`kici compile`) | Load your workflow modules, validate the DAG, assign step IDs, analyze dynamic-value purity, emit `kici.lock.json` | Before anything is pushed |
42
- | **Orchestrator** | Your orchestrator (no repo clone) | Match triggers against the lock, evaluate **pure** inline dynamic values in a sandboxed JavaScript VM, dispatch jobs | On each incoming event |
43
- | **Agent** | An ephemeral agent (fresh clone per job) | Load the workflow module, evaluate job and step rules, run step bodies and hooks, run impure dynamic-value init jobs and `dynamicJob` generators (both forms) | After dispatch |
39
+ | Phase | Where it runs | What runs | When |
40
+ | ---------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
41
+ | **Compile** | Your dev machine or CI (`kici compile`) | Load your workflow modules, validate the DAG, assign step IDs, emit `kici.lock.json` | Before anything is pushed |
42
+ | **Orchestrator** | Your orchestrator (no repo clone) | Match triggers against the lock and dispatch jobs; it never evaluates workflow code | On each incoming event |
43
+ | **Agent** | An ephemeral agent (fresh clone per job) | Load the workflow module, evaluate job and step rules, run step bodies and hooks, run dynamic-value init steps and `dynamicJob` generators (both forms) | After dispatch |
44
44
 
45
45
  The lock file is the seam. Everything left of it is decided once at compile time and frozen into JSON; everything right of it reads that JSON. See [the lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/) and [the three-tier architecture](https://docs.kici.dev/architecture/overview/) for the wider picture.
46
46
 
47
47
  ## Compile time
48
48
 
49
- `kici compile` loads your `.kici/workflows/*.ts`, validates dependencies (no cycles, no missing `needs`), assigns compile-time step IDs (unnamed steps become `step-1`, `step-2`, …), runs purity analysis over every dynamic-value function, and writes `kici.lock.json`.
49
+ `kici compile` loads your `.kici/workflows/*.ts`, validates dependencies (no cycles, no missing `needs`), assigns compile-time step IDs (unnamed steps become `step-1`, `step-2`, …), and writes `kici.lock.json`.
50
50
 
51
- The compiler runs your module's **top-level code** to build the workflow object — but that execution's side effects and in-memory state do not travel. Only the resulting workflow structure (plus the serialized source of pure dynamic-value functions) lands in the lock. Anything your top-level code computes that isn't part of the returned workflow object simply doesn't exist past this point.
51
+ The compiler runs your module's **top-level code** to build the workflow object — but that execution's side effects and in-memory state do not travel. Only the resulting workflow structure lands in the lock. Anything your top-level code computes that isn't part of the returned workflow object simply doesn't exist past this point.
52
52
 
53
53
  See [compile the workflow](https://docs.kici.dev/user/getting-started/#compile-the-workflow) for the command in context.
54
54
 
@@ -59,7 +59,7 @@ The lock is portable JSON. It carries:
59
59
  - Workflow and trigger metadata.
60
60
  - The job and step DAG, with compile-time step IDs.
61
61
  - Static values, verbatim.
62
- - The **source text** of pure dynamic-value functions, as inline expressions.
62
+ - Markers noting which fields are dynamic, so the orchestrator knows to resolve them on the agent's init step.
63
63
 
64
64
  It does **not** carry:
65
65
 
@@ -72,11 +72,13 @@ The consequence is blunt: if a value isn't in the lock, the orchestrator can't s
72
72
 
73
73
  ## Orchestrator time
74
74
 
75
- On each event the orchestrator matches triggers using only the lock — it never clones your repository. Pure dynamic `context`, `env`, and `concurrencyGroup` functions are evaluated here, as inline expressions in a sandboxed JavaScript VM (~0ms overhead), instead of dispatching a separate job to resolve them.
75
+ On each event the orchestrator matches triggers using only the lock — it never clones your repository and never evaluates workflow code. Dynamic `context`, `env`, and `concurrencyGroup` functions are not run here: the orchestrator dispatches a short init step to an agent to resolve them (see below).
76
76
 
77
- A runtime error in an inline expression fails the job immediately there is no automatic fallback to the clone-and-evaluate path. The orchestrator does **not** run `dynamicJob` generator bodies itself: for the event-only (function) form it dispatches a dedicated dynamic-evaluation job to an agent at event time; the generator function then runs agent-side (see below).
77
+ Trigger matching can query the **contents** of individual source files, not just their paths: a `pr()`, `push()`, or `tag()` trigger with a [`requires`](https://docs.kici.dev/user/sdk/triggers/#content-requirements-requires) filter is matched by reading the named files at the event's commit and evaluating the filter as declarative data still with no repository clone and no workflow code executed. A `requires` regex is checked for catastrophic (ReDoS) shapes at `kici compile` time and rejected there, so only safe patterns reach the orchestrator.
78
78
 
79
- See [dynamic values](https://docs.kici.dev/user/dynamic-values/) for the exact rules that make a function pure or impure.
79
+ The orchestrator also does **not** run `dynamicJob` generator bodies itself: for the event-only (function) form it dispatches a dedicated dynamic-evaluation job to an agent at event time; the generator function then runs agent-side (see below).
80
+
81
+ See [dynamic values](https://docs.kici.dev/user/dynamic-values/) for how dynamic `context`, `env`, and `concurrencyGroup` functions resolve.
80
82
 
81
83
  ## Agent time
82
84
 
@@ -84,7 +86,7 @@ After dispatch, each job runs in its own ephemeral agent sandbox: a shallow clon
84
86
 
85
87
  1. **Job-level rules** are evaluated. By this point the agent has already spawned and the source has already been restored, so a job that its rules skip has **still** paid for that spawn and clone; only its steps are avoided.
86
88
  2. **Step-level rules**, then each step's `run()` body and its hooks.
87
- 3. **Impure** dynamic values are resolved here too, via an init job that clones and evaluates the function (~5–10s) before the real job runs.
89
+ 3. **Dynamic values** (`context`, `env`, `concurrencyGroup` functions) are resolved here, via a short `__init__` job that runs the function before the real job runs; this shows in the run timeline as an `Init:` entry.
88
90
  4. **`dynamicJob` generators run here — both forms.** The event-only (function) form runs in a dedicated evaluation job dispatched at event time; the result-aware (options) form is deferred until its declared `needs` complete, then run with the upstream outputs frozen as `ctx.needs`.
89
91
 
90
92
  See [job execution](https://docs.kici.dev/architecture/execution/job-execution/) and [hooks and rules](https://docs.kici.dev/user/hooks/) for the details.
@@ -94,8 +96,7 @@ See [job execution](https://docs.kici.dev/architecture/execution/job-execution/)
94
96
  | Construct | Runs on | When |
95
97
  | ---------------------------- | ---------------- | ------------------------------- |
96
98
  | Static value | Compile → lock | Never re-evaluated |
97
- | Pure dynamic value | Orchestrator VM | Per event |
98
- | Impure dynamic value | Agent init job | Per event |
99
+ | Dynamic value | Agent init step | Per event |
99
100
  | Job-level rules | Agent | After clone |
100
101
  | Step-level rules | Agent | Per step |
101
102
  | `dynamicJob` (function form) | Agent (eval job) | Dispatched at event time |
@@ -134,12 +135,11 @@ Outputs are typed across the job boundary too: reading `jobRef.result.…` or `c
134
135
 
135
136
  ## Common footguns
136
137
 
137
- | Symptom | Why | Fix |
138
- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
139
- | A top-level `let seen = 0` (or a cache filled in job A) is empty in job B | Each job loads the workflow module fresh in its own agent process after its own clone — there is no shared memory between jobs | Pass data through step/job **outputs** (`OutputProxy` / `needs`), not module variables |
140
- | `context: (event) => event.ref + SUFFIX`, where `SUFFIX` is a module constant, silently falls back to the slower init-job path | Purity analysis only allows the function's own params, locals, and a fixed safe-globals set — a free identifier makes it impure | Inline the constant, or accept the init-job path knowingly. See [pure functions](https://docs.kici.dev/user/dynamic-values/#pure-functions-inline-evaluation) |
141
- | Fan-out job identities shift between re-evaluations | `ctx.event` / `ctx.needs` are frozen and replayed, but `Date.now()` / `Math.random()` are not | Derive job identity only from the frozen event/needs snapshot |
142
- | A rule-skipped job still spawned an agent and cloned | Job-level rules evaluate agent-side, after dispatch and clone — not on the orchestrator | This is by design: rules can read true runtime context (`$`, `changedFiles`, `env`). See [step-level rules](https://docs.kici.dev/user/hooks/#step-level-rules) |
138
+ | Symptom | Why | Fix |
139
+ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | A top-level `let seen = 0` (or a cache filled in job A) is empty in job B | Each job loads the workflow module fresh in its own agent process after its own clone — there is no shared memory between jobs | Pass data through step/job **outputs** (`OutputProxy` / `needs`), not module variables |
141
+ | Fan-out job identities shift between re-evaluations | `ctx.event` / `ctx.needs` are frozen and replayed, but `Date.now()` / `Math.random()` are not | Derive job identity only from the frozen event/needs snapshot |
142
+ | A rule-skipped job still spawned an agent and cloned | Job-level rules evaluate agent-side, after dispatch and clone not on the orchestrator | This is by design: rules can read true runtime context (`$`, `changedFiles`, `env`). See [step-level rules](https://docs.kici.dev/user/hooks/#step-level-rules) |
143
143
 
144
144
  ## See also
145
145
 
@@ -1080,12 +1080,16 @@ export default workflow('ci', {
1080
1080
 
1081
1081
  Rule check functions receive a `RuleContext` with:
1082
1082
 
1083
- | Property | Type | Description |
1084
- | -------------- | ----------------------------------- | ----------------------------------- |
1085
- | `event` | `EventPayload` | The triggering event data |
1086
- | `changedFiles` | `string[]` | Files changed in this event |
1087
- | `env` | `Record<string, string\|undefined>` | Environment variables |
1088
- | `$` | zx shell | Shell executor for running commands |
1083
+ | Property | Type | Description |
1084
+ | -------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------- |
1085
+ | `event` | `EventPayload` | The triggering event data |
1086
+ | `changedFiles` | `string[]` | Files changed in this event |
1087
+ | `sourceRepo` | `RepoInfo \| undefined` | The repo whose event triggered the run, when the evaluation has a checkout |
1088
+ | `workflowRepo` | `RepoInfo \| undefined` | The repo that registered the workflow. The same repo as `sourceRepo` outside a global workflow |
1089
+ | `env` | `Record<string, string\|undefined>` | Environment variables |
1090
+ | `$` | zx shell | Shell executor for running commands |
1091
+
1092
+ `RepoInfo` carries `path` — an absolute path to that repo's checkout — plus optional `ref` and `sha`. In a [global workflow](https://docs.kici.dev/user/global-workflows/) the two are different repos, which is what lets one rule read the source repo's tree while the workflow lives elsewhere. Read _through_ `path`: its contents are stable, but the path itself differs between the evaluation and the later run.
1089
1093
 
1090
1094
  ### Marker rules
1091
1095
 
@@ -2105,22 +2109,24 @@ function workflow(name: string, options: WorkflowOptions): Workflow;
2105
2109
 
2106
2110
  **Parameters:**
2107
2111
 
2108
- | Parameter | Type | Required | Description |
2109
- | --------------------- | ---------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
2110
- | `name` | `string` | yes | Unique workflow name |
2111
- | `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators |
2112
- | `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger |
2113
- | `options.rules` | `Rule[]` | no | Conditions that must pass for execution |
2114
- | `options.description` | `string` | no | Human-readable description |
2115
- | `options.hashFiles` | `string[]` | no | Extra repo-relative paths or globs mixed into the workflow content hash. Changes invalidate the source cache. |
2116
- | `options.registries` | `Registry[]` | no | Private npm registries the agent authenticates against before `npm install`. Each `tokenSecret` uses qualified `<context>:<secret>` syntax. |
2117
- | `options.installEnv` | `string[]` | no | Qualified `<context>:<secret>` refs projected as env vars onto the install subprocess (used with a customer-committed `.kici/.npmrc`). |
2118
- | `options.onCancel` | `HookInput` | no | Runs when the workflow is cancelled |
2119
- | `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel) |
2120
- | `options.onSuccess` | `HookInput` | no | Runs on workflow success |
2121
- | `options.onFailure` | `HookInput` | no | Runs on workflow failure |
2122
- | `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](https://docs.kici.dev/user/concurrency/). |
2123
- | `options.timeout` | `number` | no | Whole-run wall-clock timeout in milliseconds across all jobs. On breach the orchestrator cancels the run and marks it timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
2112
+ | Parameter | Type | Required | Description |
2113
+ | --------------------- | ---------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2114
+ | `name` | `string` | yes | Unique workflow name |
2115
+ | `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators |
2116
+ | `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger |
2117
+ | `options.rules` | `Rule[]` | no | Conditions that must pass for execution |
2118
+ | `options.filter` | `FilterFn` | no | Pre-dispatch predicate deciding whether the workflow applies to the event's source repo. A `false` result suppresses the workflow's jobs -- none is dispatched and none is reported as skipped. See [Global workflows](https://docs.kici.dev/user/global-workflows/). |
2119
+ | `options.description` | `string` | no | Human-readable description |
2120
+ | `options.hashFiles` | `string[]` | no | Extra repo-relative paths or globs mixed into the workflow content hash. Changes invalidate the source cache. |
2121
+ | `options.registries` | `Registry[]` | no | Private npm registries the agent authenticates against before `npm install`. Each `tokenSecret` uses qualified `<context>:<secret>` syntax. |
2122
+ | `options.installEnv` | `string[]` | no | Qualified `<context>:<secret>` refs projected as env vars onto the install subprocess (used with a customer-committed `.kici/.npmrc`). |
2123
+ | `options.onCancel` | `HookInput` | no | Runs when the workflow is cancelled |
2124
+ | `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel) |
2125
+ | `options.onSuccess` | `HookInput` | no | Runs on workflow success |
2126
+ | `options.onFailure` | `HookInput` | no | Runs on workflow failure |
2127
+ | `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](https://docs.kici.dev/user/concurrency/). |
2128
+ | `options.timeout` | `number` | no | Whole-run wall-clock timeout in milliseconds across all jobs. On breach the orchestrator cancels the run and marks it timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
2129
+ | `options.approval` | `ApprovalConfig` | no | Pause for a manual human approval before the whole workflow dispatches. See [Approval gates](https://docs.kici.dev/user/approvals/). |
2124
2130
 
2125
2131
  **Returns:** `Workflow` -- an immutable workflow definition.
2126
2132
 
@@ -2146,34 +2152,42 @@ function job(options: JobOptions): Job;
2146
2152
 
2147
2153
  **Parameters:**
2148
2154
 
2149
- | Parameter | Type | Required | Description |
2150
- | -------------------------- | ---------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2151
- | `name` | `string` | no | Job name (auto-generated UUID if omitted) |
2152
- | `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
2153
- | `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
2154
- | `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](https://docs.kici.dev/user/sdk/core/#single-step-job-shorthand). Mutually exclusive with `steps`. |
2155
- | `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs) |
2156
- | `options.rules` | `Rule[]` | no | Conditions for conditional execution |
2157
- | `options.description` | `string` | no | Human-readable description |
2158
- | `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
2159
- | `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
2160
- | `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
2161
- | `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
2162
- | `options.container` | `string \| ContainerConfig` | no | Docker image for job execution. String form is the image name; object form adds `env`. All steps run inside the container. |
2163
- | `options.sandbox` | `{ capabilities?: string[]; network?: 'default' \| 'none' \| 'host' }` | no | Per-job container sandbox escape hatch (container jobs only). Request extra Linux capabilities / host networking; granted only within your operator's allow-list, else the run fails at dispatch. See below. |
2164
- | `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
2165
- | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
2166
- | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](https://docs.kici.dev/user/concurrency/). |
2167
- | `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
2168
- | `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
2169
- | `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
2170
- | `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
2171
- | `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
2172
- | `options.afterStep` | `HookInput` | no | Hook that runs after each step |
2173
- | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](https://docs.kici.dev/user/hooks/#hook-timeout). |
2174
- | `options.timeout` | `number` | no | Total job wall-clock timeout in milliseconds (init + all steps + hooks). On breach the job is aborted and reported timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
2175
- | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](https://docs.kici.dev/user/sdk/core/#per-job-resources) below. |
2176
- | `options.init` | `InitConfig` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. A generic config, a typed preset (`'mise'` / `{ mise }`), `'auto'`, or `false`. See [Per-job init](https://docs.kici.dev/user/sdk/core/#per-job-init) below. |
2155
+ | Parameter | Type | Required | Description |
2156
+ | ------------------------------ | -------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2157
+ | `name` | `string` | no | Job name (auto-generated UUID if omitted) |
2158
+ | `options.runsOn` | `RunsOn` | yes (or `runsOnAll`) | Single-agent targeting -- runner label(s) and optional exclusions (see below). Mutually exclusive with `runsOnAll`. |
2159
+ | `options.runsOnAll` | `RunsOnAllInput` | yes (or `runsOn`) | Host fan-out -- one pinned execution per roster host matching the predicate. Mutually exclusive with `runsOn`. See [runsOnAll host fan-out](https://docs.kici.dev/user/sdk/runs-on-all/). |
2160
+ | `options.onUnreachable` | `'skip' \| 'fail' \| 'hold'` | no (default: `hold`) | Failure policy for unreachable durable hosts. `skip` omits them, `fail` fails the run, `hold` queues a pinned child and waits. Only meaningful alongside `runsOnAll`. |
2161
+ | `options.includeUninitialized` | `boolean` | no (default: `false`) | Widen a `runsOnAll` fan-out to declared-but-un-agented hosts -- a matching host with no live agent gets a temporary init-runner brought up over SSH. Only meaningful alongside `runsOnAll`. |
2162
+ | `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
2163
+ | `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](https://docs.kici.dev/user/sdk/core/#single-step-job-shorthand). Mutually exclusive with `steps`. |
2164
+ | `options.needs` | `Array<Job \| string \| DynamicGroupRef \| { name; when? } \| { group; when? }>` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs) |
2165
+ | `options.rules` | `Rule[]` | no | Conditions for conditional execution |
2166
+ | `options.description` | `string` | no | Human-readable description |
2167
+ | `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
2168
+ | `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
2169
+ | `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
2170
+ | `options.maxParallel` | `number` | no | Fan-out concurrency width -- the maximum number of fan-out children (matrix combinations or `runsOnAll` hosts) running at once. A sliding window; `1` is strictly serial. Must be `>= 1`. |
2171
+ | `options.failFast` | `boolean` | no (default: `false`) | Halt the fan-out on the first child failure: stop releasing new children and skip the ones still held. Applies to both matrix and `runsOnAll` fan-out. |
2172
+ | `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
2173
+ | `options.container` | `string \| ContainerConfig` | no | Docker image for job execution. String form is the image name; object form adds `env`. All steps run inside the container. |
2174
+ | `options.sandbox` | `{ capabilities?: string[]; network?: 'default' \| 'none' \| 'host' }` | no | Per-job container sandbox escape hatch (container jobs only). Request extra Linux capabilities / host networking; granted only within your operator's allow-list, else the run fails at dispatch. See below. |
2175
+ | `options.context` | `string \| ((event) => string \| Promise<string>)` | no | Bound context for this job -- the secret / variable scope it resolves against. Static string or async/dynamic function -- see [Contexts](https://docs.kici.dev/user/contexts/) and [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
2176
+ | `options.contexts` | `(string \| ((event) => string \| Promise<string>))[]` | no | Bound contexts in merge order (later entries override earlier on name collisions). Mutually exclusive with `context`. |
2177
+ | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
2178
+ | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name. Defaults to the first bound context's name -- see [Concurrency](https://docs.kici.dev/user/concurrency/). |
2179
+ | `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
2180
+ | `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
2181
+ | `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
2182
+ | `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
2183
+ | `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
2184
+ | `options.afterStep` | `HookInput` | no | Hook that runs after each step |
2185
+ | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](https://docs.kici.dev/user/hooks/#hook-timeout). |
2186
+ | `options.timeout` | `number` | no | Total job wall-clock timeout in milliseconds (init + all steps + hooks). On breach the job is aborted and reported timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
2187
+ | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](https://docs.kici.dev/user/sdk/core/#per-job-resources) below. |
2188
+ | `options.init` | `InitConfig` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. A generic config, a typed preset (`'mise'` / `{ mise }`), `'auto'`, or `false`. See [Per-job init](https://docs.kici.dev/user/sdk/core/#per-job-init) below. |
2189
+ | `options.cache` | `CacheInput` | no | Declarative cache: restored before steps, saved after the job on a key miss. See [Caching](https://docs.kici.dev/user/sdk/caching/). |
2190
+ | `options.approval` | `ApprovalConfig` | no | Pause for a manual human approval before this job dispatches. See [Approval gates](https://docs.kici.dev/user/approvals/). |
2177
2191
 
2178
2192
  **Returns:** `Job` -- an immutable job definition.
2179
2193
 
@@ -3089,13 +3103,15 @@ type RuleCheckFn = (ctx: RuleContext) => Promise<boolean> | boolean;
3089
3103
 
3090
3104
  Can be sync or async. Receives a `RuleContext`:
3091
3105
 
3092
- | Property | Type | Description |
3093
- | -------------------- | ----------------------------------------- | --------------------------------------------------------------------- |
3094
- | `event` | `EventPayload` | The triggering event payload (discriminated union — narrow on `type`) |
3095
- | `changedFiles` | `string[]` | Files changed in this event (see availability note below) |
3096
- | `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` is available |
3097
- | `env` | `Record<string, string\|undefined>` | Environment variables |
3098
- | `$` | zx shell | Shell executor for running commands |
3106
+ | Property | Type | Description |
3107
+ | -------------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
3108
+ | `event` | `EventPayload` | The triggering event payload (discriminated union — narrow on `type`) |
3109
+ | `changedFiles` | `string[]` | Files changed in this event (see availability note below) |
3110
+ | `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` is available |
3111
+ | `sourceRepo` | `RepoInfo \| undefined` | Repo whose event triggered the run, when the evaluation has a checkout |
3112
+ | `workflowRepo` | `RepoInfo \| undefined` | Repo that registered the workflow (same repo outside a global workflow) |
3113
+ | `env` | `Record<string, string\|undefined>` | Environment variables |
3114
+ | `$` | zx shell | Shell executor for running commands |
3099
3115
 
3100
3116
  `changedFiles` is available on `push` and `pull_request` events — the agent computes the diff from its checkout, so no `paths:` trigger is required. It is `unavailable` for events with no diff (`schedule`, `tag`, `manual_schedule`), and in the rare case where the diff cannot be computed (e.g. a history deeper than the agent's bounded fetch). Reading `changedFiles` when it is unavailable throws and fails the job, so guard with `changedFilesStatus` first when a rule can run on such events:
3101
3117
 
@@ -3106,6 +3122,8 @@ rule('has source changes', (ctx) => {
3106
3122
  });
3107
3123
  ```
3108
3124
 
3125
+ The throw is a `ChangedFilesUnavailableError` (exported from `@kici-dev/sdk`, carrying the `changedFilesStatus` and `eventType` that produced it). `evaluateRules()` deliberately re-throws it rather than folding it into a `passed=false` skip, so a path-based gate fails loudly instead of silently mis-evaluating.
3126
+
3109
3127
  ### evaluateRules(rules, context, label, onRuleResult?)
3110
3128
 
3111
3129
  Evaluate an array of rules sequentially with fail-fast behavior. Stops on the first failure.
@@ -3378,12 +3396,18 @@ type DynamicJobFn = (context: DynamicJobContext) => Promise<Job[]>;
3378
3396
 
3379
3397
  Receives a `DynamicJobContext`:
3380
3398
 
3381
- | Property | Type | Description |
3382
- | -------- | ----------------------------------- | --------------------------- |
3383
- | `$` | zx shell | Shell executor |
3384
- | `ctx` | `{ workflow, event? }` | Workflow metadata and event |
3385
- | `log` | `Logger` | Structured logger |
3386
- | `env` | `Record<string, string\|undefined>` | Environment variables |
3399
+ | Property | Type | Description |
3400
+ | -------------- | ----------------------------------- | ----------------------------------------------------------------------- |
3401
+ | `$` | zx shell | Shell executor |
3402
+ | `ctx` | `{ workflow, event? }` | Workflow metadata and event |
3403
+ | `log` | `Logger` | Structured logger |
3404
+ | `env` | `Record<string, string\|undefined>` | Environment variables |
3405
+ | `sourceRepo` | `RepoInfo \| undefined` | Repo whose event triggered the run, when the evaluation has a checkout |
3406
+ | `workflowRepo` | `RepoInfo \| undefined` | Repo that registered the workflow (same repo outside a global workflow) |
3407
+
3408
+ `RepoInfo` carries `path` — an absolute path to that repo's checkout — plus optional `ref` and `sha`; guard before reading either, since an event that carries no single ref leaves them undefined. In a [global workflow](https://docs.kici.dev/user/global-workflows/) `sourceRepo` and `workflowRepo` are different repos, so one generator can produce a different job set per source repo.
3409
+
3410
+ **`sourceRepo.path` is not stable across calls.** A generator is invoked once to discover the job set and again to extract the step closures of the job being run; both see the same tree at the same commit, but not necessarily the same path or even the same machine. Read _through_ it, and derive job names from what the tree contains — never from the path itself, or the second call produces different names and the run fails the determinism check.
3387
3411
 
3388
3412
  ```typescript
3389
3413
  const discoverJobs: DynamicJobFn = async ({ $ }) => {
@@ -3576,6 +3600,182 @@ workflow runs rather than being silently dropped, and the delivery is recorded
3576
3600
  as degraded. GitHub always provides an exact list; a transient API error fails
3577
3601
  loudly, not as empty.
3578
3602
 
3603
+ ### Content requirements (`requires`)
3604
+
3605
+ Where `paths` filters on **which files changed**, `requires` filters on **what
3606
+ those files contain**. It is a declarative filter on the `pr()`, `push()`, and
3607
+ `tag()` triggers: a list of queries over the bytes of named source files, read
3608
+ at the event's commit. The orchestrator evaluates it as pure data before
3609
+ dispatching — it reads only the referenced files, never clones the whole
3610
+ repository, and never runs any of your workflow code. A workflow whose `requires`
3611
+ does not pass is simply not dispatched.
3612
+
3613
+ Each entry is a `ContentRequirement`:
3614
+
3615
+ ```typescript
3616
+ interface ContentRequirement {
3617
+ file: string; // repo-relative path to query
3618
+ format?: 'auto' | 'json' | 'yaml' | 'text'; // how to parse the file (default: 'auto')
3619
+ exists?: string[]; // JSONPath expressions that must each resolve to ≥1 node (json/yaml)
3620
+ match?: Record<string, unknown>; // JSONPath → expected value; every one must match (json/yaml)
3621
+ not?: Record<string, unknown>; // JSONPath → value; passes only when NONE match (json/yaml)
3622
+ contains?: string | string[]; // literal substrings, all of which must appear (text only)
3623
+ notContains?: string | string[]; // literal substrings, none of which may appear (text only)
3624
+ matches?: string | RegExp | (string | RegExp)[]; // regexes, all of which must match (text only)
3625
+ notMatches?: string | RegExp | (string | RegExp)[]; // regexes, none of which may match (text only)
3626
+ ignoreCase?: boolean; // applies to contains/notContains only (default: false)
3627
+ absent?: boolean; // passes only when the file does NOT exist
3628
+ }
3629
+ ```
3630
+
3631
+ **Format.** `format: 'auto'` (the default) picks the parser by extension:
3632
+ `.json` → JSON, `.yaml` / `.yml` → YAML, everything else → text. Set `format`
3633
+ explicitly to override — e.g. treat an extensionless file as JSON, or read a
3634
+ `.json` file as raw text. JSON and YAML both parse to an object, so the JSONPath
3635
+ keys (`exists` / `match` / `not`) work identically over either; `text` files are
3636
+ queried by `contains`, `notContains`, `matches`, and `notMatches` over the raw
3637
+ bytes.
3638
+
3639
+ **Query keys.**
3640
+
3641
+ - **`exists`** — an array of JSONPath expressions; each must resolve to at least
3642
+ one node in the parsed document.
3643
+ - **`match`** — a JSONPath → expected-value map; every expression must match. An
3644
+ expected value is an exact value, a regex string in `/pattern/flags` form
3645
+ (against a string node), or an array of acceptable values (any one matches).
3646
+ - **`not`** — the same map shape, inverted: the entry passes only when **none** of
3647
+ the expressions match.
3648
+ - **`contains` / `notContains`** — literal substrings tested against the raw file
3649
+ text. Every entry must be present (`contains`) or absent (`notContains`). No
3650
+ escaping needed (text format only).
3651
+ - **`matches`** — one or several regexes (a `RegExp` or `/pattern/flags` string),
3652
+ each of which must match the raw file text (text format only).
3653
+ - **`notMatches`** — the inverse of `matches`: the entry passes only when none of
3654
+ the regexes match (text format only).
3655
+ - **`ignoreCase`** — case-insensitive `contains` / `notContains` only; a regex
3656
+ carries its own flags. Default false.
3657
+ - **`absent: true`** — passes only when the file does **not** exist at the event's
3658
+ commit. It is mutually exclusive with the query keys above.
3659
+ - A bare `{ file }` with no query key requires the file to **exist**.
3660
+
3661
+ The keys inside one entry are AND-ed, and the entries in a `requires` list are
3662
+ AND-ed with each other. An empty or absent `requires` matches everything, exactly
3663
+ like `paths`.
3664
+
3665
+ **Examples:**
3666
+
3667
+ ```typescript
3668
+ // Only run CI when package.json declares a `ci` script.
3669
+ push({ branches: 'main', requires: [{ file: 'package.json', exists: ['$.scripts.ci'] }] });
3670
+
3671
+ // Deploy only when the service config enables it (YAML, matched by value).
3672
+ push({
3673
+ branches: 'main',
3674
+ requires: [{ file: 'service.yaml', match: { '$.deploy.enabled': true } }],
3675
+ });
3676
+
3677
+ // Only run when the Dockerfile builds from a Node base image (raw-text regex).
3678
+ pr({ requires: [{ file: 'Dockerfile', format: 'text', matches: '/^FROM node:/m' }] });
3679
+
3680
+ // Skip the workflow whenever a repo carries an opt-out marker file.
3681
+ push({ requires: [{ file: '.skip-ci', absent: true }] });
3682
+
3683
+ // Combine filters: a tag build that requires a version file AND forbids a draft flag.
3684
+ tag({
3685
+ patterns: ['v*'],
3686
+ requires: [
3687
+ { file: 'VERSION', matches: '/^\\d+\\.\\d+\\.\\d+$/' },
3688
+ { file: 'release.json', not: { '$.draft': true } },
3689
+ ],
3690
+ });
3691
+ ```
3692
+
3693
+ **Fail-visible evaluation.** Files are read at the event's commit. If a
3694
+ referenced file is larger than **1 MiB**, or fails to parse for its format, the
3695
+ requirement is **indeterminate** — the candidate workflow is dropped and does
3696
+ **not** run. A `requires` that cannot be evaluated never silently passes.
3697
+
3698
+ **Compile-time validation.** `kici compile` rejects a malformed requirement before
3699
+ it ever reaches the orchestrator: a raw-text key that is invalid or catastrophic
3700
+ (ReDoS-prone) is rejected by a safe-regex check; a text file cannot carry a
3701
+ JSON/YAML query key (`exists` / `match` / `not`) and a JSON/YAML file cannot carry a
3702
+ raw-text key; `absent` cannot be combined with a query key; and an explicit
3703
+ `format` with no query key is rejected as having nothing to check.
3704
+
3705
+ ### Commit-message filters (`commitMessage`)
3706
+
3707
+ Where `requires` filters on what the repository's **files** contain,
3708
+ `commitMessage` filters on what the **event** says. It is a declarative filter on
3709
+ the `pr()`, `push()`, and `tag()` triggers. The orchestrator evaluates it
3710
+ directly from the webhook payload: no file is fetched, and no repository is
3711
+ cloned. For an organization-wide workflow it dispatches no evaluation job. It is
3712
+ the cheapest gate available.
3713
+
3714
+ The text it tests is the **full head-commit message** — subject and body — for
3715
+ `push` and `tag`, and the **title plus body** for pull-request events.
3716
+
3717
+ ```typescript
3718
+ interface TextMatch {
3719
+ contains?: string | string[]; // every needle must be present
3720
+ notContains?: string | string[]; // no needle may be present
3721
+ matches?: string | RegExp | (string | RegExp)[]; // every regex must match
3722
+ notMatches?: string | RegExp | (string | RegExp)[]; // no regex may match
3723
+ ignoreCase?: boolean; // applies to contains/notContains only (default: false)
3724
+ }
3725
+ ```
3726
+
3727
+ **Every entry in a list is a conjunct.** `contains: ['a', 'b']` passes only when
3728
+ the text contains both, and the keys AND together. To express OR, declare two
3729
+ triggers — a workflow's trigger list already matches on the first one that fits:
3730
+
3731
+ ```typescript
3732
+ // AND — one trigger.
3733
+ push({ commitMessage: { contains: ['release:', 'approved'] } });
3734
+
3735
+ // OR — two triggers.
3736
+ on: [
3737
+ push({ branches: 'main', commitMessage: { contains: 'deploy:' } }),
3738
+ push({ branches: 'main', commitMessage: { contains: 'release:' } }),
3739
+ ];
3740
+ ```
3741
+
3742
+ Needles are **literal substrings** — no glob, no regex, no escaping, so a needle
3743
+ containing `.*` matches only the literal `.*`. Use `matches` / `notMatches` for a
3744
+ pattern; both accept a `RegExp` literal, and the `m` flag reaches the body:
3745
+
3746
+ ```typescript
3747
+ // The single most common use: skip marker commits.
3748
+ push({ branches: 'main', commitMessage: { notContains: ['[skip ci]', '[ci skip]'] } });
3749
+
3750
+ // Ignore dependency-bump noise across an organization.
3751
+ push({ commitMessage: { notMatches: /^chore\(deps\):/ } });
3752
+
3753
+ // Require a conventional-commit prefix and forbid a WIP marker.
3754
+ pr({ target: 'main', commitMessage: { matches: /^(feat|fix)\(/, notContains: 'WIP' } });
3755
+
3756
+ // Match a trailer in the commit body.
3757
+ push({ commitMessage: { matches: /^Fixes: #\d+$/m } });
3758
+ ```
3759
+
3760
+ `ignoreCase` affects `contains` and `notContains` only — a regex already carries
3761
+ its own flags, so write `/^feat:/i` rather than expecting `ignoreCase` to reach
3762
+ it.
3763
+
3764
+ **Fail-visible evaluation.** Some events carry no message at all. A
3765
+ branch-deletion push has no head commit, and a self-hosted forge may publish
3766
+ none. The trigger then does **not** match, and the decision trace records it as
3767
+ `indeterminate` rather than as an exclusion. A `commitMessage` filter that cannot
3768
+ be evaluated never silently passes.
3769
+
3770
+ **Compile-time validation.** `kici compile` rejects a malformed matcher. It
3771
+ refuses:
3772
+
3773
+ - a matcher with no query key;
3774
+ - an `ignoreCase` that would affect nothing;
3775
+ - an empty needle list;
3776
+ - an empty-string needle (it would match every text);
3777
+ - a regex that is invalid or catastrophic (ReDoS-prone).
3778
+
3579
3779
  ### tag()
3580
3780
 
3581
3781
  Create a tag trigger. Returns a frozen `TagTriggerConfig`.
@@ -5499,7 +5699,7 @@ When `--target` narrows a `runsOnAll` job to zero hosts, the run **fails** by de
5499
5699
  (a mistyped selector should be loud, not silently no-op). Pass `--target-allow-empty`
5500
5700
  to **skip** the zeroed job instead — it records a `skipped` status, and downstream jobs
5501
5701
  gated with `when: 'on-skip'` (or `when: 'always'`) still run, exactly as for an
5502
- `onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](https://docs.kici.dev/user/cli-reference/#host-narrowing-with---target)
5702
+ `onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](https://docs.kici.dev/user/cli/runs-and-approvals/#host-narrowing-with---target)
5503
5703
  for the full flag behavior and the [`needs` gating model](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs)
5504
5704
  for how a skipped upstream propagates.
5505
5705
 
@@ -5543,41 +5743,42 @@ All types are exported from `@kici-dev/sdk` as type-only imports.
5543
5743
 
5544
5744
  ### Trigger types
5545
5745
 
5546
- | Type | Description |
5547
- | ------------------------------- | --------------------------------------------------------------------- |
5548
- | `Trigger` | Trigger definition (trigger config + source location) |
5549
- | `TriggerConfig` | Union of all 23 trigger config types |
5550
- | `PrTriggerConfig` | PR trigger configuration (from `pr()`) |
5551
- | `PushTriggerConfig` | Push trigger configuration (from `push()`) |
5552
- | `TagTriggerConfig` | Tag trigger configuration (from `tag()`) |
5553
- | `CommentTriggerConfig` | Comment trigger configuration (from `comment()`) |
5554
- | `ReviewTriggerConfig` | Review trigger configuration (from `review()`) |
5555
- | `ReviewCommentTriggerConfig` | Review comment trigger configuration (from `reviewComment()`) |
5556
- | `ReleaseTriggerConfig` | Release trigger configuration (from `release()`) |
5557
- | `DispatchTriggerConfig` | Repository dispatch trigger configuration (from `dispatch()`) |
5558
- | `CreateTriggerConfig` | Ref creation trigger configuration (from `create()`) |
5559
- | `DeleteTriggerConfig` | Ref deletion trigger configuration (from `delete()`) |
5560
- | `StatusTriggerConfig` | Commit status trigger configuration (from `status()`) |
5561
- | `WorkflowRunTriggerConfig` | Workflow run trigger configuration (from `workflowRun()`) |
5562
- | `ForkTriggerConfig` | Fork trigger configuration (from `fork()`) |
5563
- | `StarTriggerConfig` | Star trigger configuration (from `star()`) |
5564
- | `WatchTriggerConfig` | Watch trigger configuration (from `watch()`) |
5565
- | `WebhookTriggerConfig` | Catch-all webhook trigger configuration (from `webhook()`) |
5566
- | `KiciEventTriggerConfig` | Custom event trigger configuration (from `kiciEvent()`) |
5567
- | `WorkflowCompleteTriggerConfig` | Workflow completion trigger configuration (from `workflowComplete()`) |
5568
- | `JobCompleteTriggerConfig` | Job completion trigger configuration (from `jobComplete()`) |
5569
- | `GenericWebhookTriggerConfig` | Generic webhook trigger configuration (from `genericWebhook()`) |
5570
- | `ScheduleTriggerConfig` | Schedule trigger configuration (from `schedule()`) |
5571
- | `LifecycleTriggerConfig` | Lifecycle trigger configuration (from `lifecycle()`) |
5572
- | `PrConfigInput` | Config object for `pr()` factory |
5573
- | `PushConfigInput` | Config object for `push()` factory |
5574
- | `BranchPattern` | `{ type: 'glob', pattern } \| { type: 'regex', pattern, flags? }` |
5575
- | `PrEvent` | PR event string literal union (17 event types) |
5576
- | `GenericWebhookConfigInput` | Config object for `genericWebhook()` factory |
5577
- | `GenericWebhookAuth` | Union of generic webhook auth types (HMAC or API key) |
5578
- | `GenericWebhookHmacAuth` | HMAC-SHA256 auth configuration for generic webhooks |
5579
- | `GenericWebhookApiKeyAuth` | API key auth configuration for generic webhooks |
5580
- | `GenericWebhookAuthMethod` | Auth method string literal (`'hmac-sha256' \| 'api-key'`) |
5746
+ | Type | Description |
5747
+ | ----------------------------------- | ------------------------------------------------------------------------------ |
5748
+ | `Trigger` | Trigger definition (trigger config + source location) |
5749
+ | `TriggerConfig` | Union of all 23 trigger config types |
5750
+ | `PrTriggerConfig` | PR trigger configuration (from `pr()`) |
5751
+ | `PushTriggerConfig` | Push trigger configuration (from `push()`) |
5752
+ | `TagTriggerConfig` | Tag trigger configuration (from `tag()`) |
5753
+ | `CommentTriggerConfig` | Comment trigger configuration (from `comment()`) |
5754
+ | `ReviewTriggerConfig` | Review trigger configuration (from `review()`) |
5755
+ | `ReviewCommentTriggerConfig` | Review comment trigger configuration (from `reviewComment()`) |
5756
+ | `ReleaseTriggerConfig` | Release trigger configuration (from `release()`) |
5757
+ | `DispatchTriggerConfig` | Repository dispatch trigger configuration (from `dispatch()`) |
5758
+ | `CreateTriggerConfig` | Ref creation trigger configuration (from `create()`) |
5759
+ | `DeleteTriggerConfig` | Ref deletion trigger configuration (from `delete()`) |
5760
+ | `StatusTriggerConfig` | Commit status trigger configuration (from `status()`) |
5761
+ | `WorkflowRunTriggerConfig` | Workflow run trigger configuration (from `workflowRun()`) |
5762
+ | `ForkTriggerConfig` | Fork trigger configuration (from `fork()`) |
5763
+ | `StarTriggerConfig` | Star trigger configuration (from `star()`) |
5764
+ | `WatchTriggerConfig` | Watch trigger configuration (from `watch()`) |
5765
+ | `WebhookTriggerConfig` | Catch-all webhook trigger configuration (from `webhook()`) |
5766
+ | `KiciEventTriggerConfig` | Custom event trigger configuration (from `kiciEvent()`) |
5767
+ | `WorkflowCompleteTriggerConfig` | Workflow completion trigger configuration (from `workflowComplete()`) |
5768
+ | `WorkflowsFailedBatchTriggerConfig` | Batched workflow-failure trigger configuration (from `workflowsFailedBatch()`) |
5769
+ | `JobCompleteTriggerConfig` | Job completion trigger configuration (from `jobComplete()`) |
5770
+ | `GenericWebhookTriggerConfig` | Generic webhook trigger configuration (from `genericWebhook()`) |
5771
+ | `ScheduleTriggerConfig` | Schedule trigger configuration (from `schedule()`) |
5772
+ | `LifecycleTriggerConfig` | Lifecycle trigger configuration (from `lifecycle()`) |
5773
+ | `PrConfigInput` | Config object for `pr()` factory |
5774
+ | `PushConfigInput` | Config object for `push()` factory |
5775
+ | `BranchPattern` | `{ type: 'glob', pattern } \| { type: 'regex', pattern, flags? }` |
5776
+ | `PrEvent` | PR event string literal union (17 event types) |
5777
+ | `GenericWebhookConfigInput` | Config object for `genericWebhook()` factory |
5778
+ | `GenericWebhookAuth` | Union of generic webhook auth types (HMAC or API key) |
5779
+ | `GenericWebhookHmacAuth` | HMAC-SHA256 auth configuration for generic webhooks |
5780
+ | `GenericWebhookApiKeyAuth` | API key auth configuration for generic webhooks |
5781
+ | `GenericWebhookAuthMethod` | Auth method string literal (`'hmac-sha256' \| 'api-key'`) |
5581
5782
 
5582
5783
  ### Rule types
5583
5784
 
@@ -7083,12 +7284,13 @@ companion at [Operator troubleshooting](https://docs.kici.dev/operator/troublesh
7083
7284
 
7084
7285
  ## Fast triage
7085
7286
 
7086
- | You see... | Jump to |
7087
- | -------------------------------------------------------------- | ------------------------------------------------------- |
7088
- | A run finishes with `No jobs dispatched` | [No jobs dispatched](https://docs.kici.dev/user/common-failures/#no-jobs-dispatched) |
7089
- | A run fails complaining the lock file is stale or incompatible | [Lock-file drift](https://docs.kici.dev/user/common-failures/#lock-file-drift) |
7090
- | You pushed but no run ever appears | [The webhook never arrives](https://docs.kici.dev/user/common-failures/#the-webhook-never-arrives) |
7091
- | A run is stuck "queued" and no agent ever picks it up | [The agent won't connect](https://docs.kici.dev/user/common-failures/#the-agent-wont-connect) |
7287
+ | You see... | Jump to |
7288
+ | -------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
7289
+ | A run finishes with `No jobs dispatched` | [No jobs dispatched](https://docs.kici.dev/user/common-failures/#no-jobs-dispatched) |
7290
+ | A run fails complaining the lock file is stale or incompatible | [Lock-file drift](https://docs.kici.dev/user/common-failures/#lock-file-drift) |
7291
+ | You pushed but no run ever appears | [The webhook never arrives](https://docs.kici.dev/user/common-failures/#the-webhook-never-arrives) |
7292
+ | A run is stuck "queued" and no agent ever picks it up | [The agent won't connect](https://docs.kici.dev/user/common-failures/#the-agent-wont-connect) |
7293
+ | A `commitMessage`-gated workflow stops running for some events | [A `commitMessage` filter never evaluates](https://docs.kici.dev/user/common-failures/#a-commitmessage-filter-never-evaluates) |
7092
7294
 
7093
7295
  ## No jobs dispatched
7094
7296
 
@@ -7228,6 +7430,24 @@ the unpullable image) — hand this to your operator with the run's failure reas
7228
7430
  The full provisioning-failure playbook is in
7229
7431
  [Operator troubleshooting](https://docs.kici.dev/operator/troubleshooting/).
7230
7432
 
7433
+ ## A `commitMessage` filter never evaluates
7434
+
7435
+ **Symptom.** A workflow gated on a `commitMessage` trigger filter stops running
7436
+ for some events, even though the message looks like it should match.
7437
+
7438
+ **Cause.** The event carries no commit message. A branch-deletion push has no
7439
+ head commit, and a self-hosted forge (Gogs, or a GitLab source) may publish none
7440
+ at the configured path. The filter is **fail-visible**: when it cannot read a
7441
+ message, the workflow does not run rather than running ungated.
7442
+
7443
+ **Diagnose.** The decision trace records the `commitMessage` check with the
7444
+ verdict `indeterminate` and the reason `no commit message in payload`. That is
7445
+ distinct from an `excluded` verdict, which the message itself caused.
7446
+
7447
+ **Fix.** For a self-hosted forge, set the source's `commitMessage` payload path so
7448
+ the orchestrator can read the head commit's message. A branch-deletion push
7449
+ genuinely carries no message and is expected not to match.
7450
+
7231
7451
  ## When to escalate to your operator
7232
7452
 
7233
7453
  The failures above are ones you can resolve from your workflow repo and the
@@ -7521,6 +7741,7 @@ Each workflow entry includes:
7521
7741
  | `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
7522
7742
  | `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
7523
7743
  | `approval` | Normalized approval gate (optional): `clauses`, `reason`, `timeoutSeconds`, `when`. When present the whole run is held before any job is dispatched. Job and step entries carry the same normalized block for job- and step-level gates. See [approval gates](https://docs.kici.dev/user/approvals/). |
7744
+ | `hasFilter` | `true` when the workflow declares a workflow-level `filter` predicate (optional; omitted rather than `false`). The predicate itself is never serialized — the flag tells the orchestrator an agent must evaluate the workflow before any of its jobs is dispatched. See [global workflows](https://docs.kici.dev/user/global-workflows/#narrowing-with-a-filter). |
7524
7745
  | Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
7525
7746
 
7526
7747
  Step entries carry their own capability flags, so the orchestrator can reason about a step without loading your TypeScript:
@@ -10623,7 +10844,7 @@ group: () => 'deploy';
10623
10844
  group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
10624
10845
  ```
10625
10846
 
10626
- The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. This differs from job-level `concurrencyGroup` (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)), where the compiler performs purity analysis and can inline pure functions for orchestrator-side evaluation.
10847
+ The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. Job-level `concurrencyGroup` functions (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)) resolve the same way on the agent's init step, never in the orchestrator.
10627
10848
 
10628
10849
  ## cancelInProgress mode
10629
10850
 
@@ -10860,7 +11081,7 @@ job('deploy-review', {
10860
11081
  });
10861
11082
  ```
10862
11083
 
10863
- A pure function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is evaluated inline at dispatch with no init-job overhead. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
11084
+ A dynamic context function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is resolved on the eval agent's init step before the job runs. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
10864
11085
 
10865
11086
  ### Multiple contexts per job
10866
11087
 
@@ -11163,22 +11384,17 @@ job('deploy', {
11163
11384
 
11164
11385
  ## How it works
11165
11386
 
11166
- When you define a dynamic value as a function, the compiler analyzes it at compile time to determine whether it is **pure** (can be evaluated without cloning the repo or running an init job).
11387
+ When you define a dynamic value as a function, it is resolved on the eval agent as a short **init** step that runs before the job:
11167
11388
 
11168
- ### Pure functions (inline evaluation)
11389
+ 1. The orchestrator dispatches a lightweight `__init__` job to an agent.
11390
+ 2. The agent loads the compiled workflow bundle and calls your function with the normalized event.
11391
+ 3. The agent reports the resolved values back to the orchestrator, which dispatches the real execution job with them applied.
11169
11392
 
11170
- A pure function is one that:
11393
+ This resolution appears in the run timeline as an `Init:` entry. The orchestrator never evaluates workflow code — every dynamic `context`, `env`, and `concurrencyGroup` function runs agent-side, whatever it references.
11171
11394
 
11172
- - Is synchronous (no `async`/`await`)
11173
- - Only references its parameters and local variables
11174
- - Does not import or require external modules
11175
- - Does not access globals like `process`, `fetch`, `console`, `setTimeout`, etc.
11176
- - Uses only safe built-in constructors: `String`, `Number`, `Boolean`, `Array`, `Object`, `JSON`, `Math`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `encodeURIComponent`, `decodeURIComponent`, `encodeURI`, `decodeURI`
11177
- - Does not use `this`, `new`, `class`, `throw`, `try`/`catch`, `delete`, `var`, `yield`, or mutation operators (`++`, `--`, `+=`, etc.)
11395
+ `kici preview` lists the injected `__init__` job under each affected job, so you can spot it before the first run.
11178
11396
 
11179
- When the compiler detects a pure function, it serializes the function source directly into the lock file as an inline expression. At dispatch time, the orchestrator evaluates the expression in a sandboxed VM context (~0ms overhead) instead of dispatching an init job.
11180
-
11181
- **Examples of pure functions:**
11397
+ **Examples:**
11182
11398
 
11183
11399
  ```typescript
11184
11400
  // Simple branch extraction
@@ -11190,65 +11406,32 @@ env: (event) => ({ BRANCH: event.targetBranch });
11190
11406
  // Concatenation with event data
11191
11407
  concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
11192
11408
 
11193
- // Using safe globals
11194
- env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
11195
-
11196
- // Local variables are fine
11409
+ // Local variables and safe globals
11197
11410
  context: (event) => {
11198
11411
  const parts = event.targetBranch.split('/');
11199
11412
  return parts[parts.length - 1];
11200
11413
  };
11201
- ```
11202
-
11203
- ### Impure functions (init-job evaluation)
11204
-
11205
- If the compiler determines a function is impure, it prints a `warning [W101]` naming the affected field (`context`, `env`, or `concurrencyGroup`), the reason the function was judged impure, and the ~5-10 second init-job cost — and compilation still succeeds. The function falls back to the two-phase init model. This means:
11206
11414
 
11207
- 1. The orchestrator dispatches a special `__init__` job to a builder agent
11208
- 2. The builder agent clones the repository and evaluates the function
11209
- 3. The resolved values are sent back to the orchestrator
11210
- 4. The orchestrator dispatches the real execution job with the resolved values
11211
-
11212
- This adds approximately 5-10 seconds of overhead for cloning and evaluation.
11213
-
11214
- `kici preview` lists the injected `__init__` job under each affected job, so you can spot the init-job cost before the first run.
11215
-
11216
- **Examples of impure functions (will use init job):**
11217
-
11218
- ```typescript
11219
- // Async functions cannot be inlined
11415
+ // Async lookups, module access, and process/global reads all work
11220
11416
  context: async (event) => await lookupEnv(event.targetBranch);
11221
-
11222
- // External module references
11223
- env: (event) => {
11224
- const config = require('./config');
11225
- return config.env;
11226
- };
11227
-
11228
- // Process/global access
11229
- context: (event) => process.env.DEFAULT_ENV || 'staging';
11230
-
11231
- // Dynamic imports
11232
- env: async (event) => {
11233
- const m = await import('./config.js');
11234
- return m.default;
11235
- };
11417
+ env: (event) => ({ DEFAULT_ENV: process.env.DEFAULT_ENV ?? 'staging' });
11236
11418
  ```
11237
11419
 
11238
- ## Performance comparison
11420
+ ## Performance
11239
11421
 
11240
- | Evaluation path | Overhead | When used |
11241
- | ------------------------------------ | -------- | --------------------------------------------------------------- |
11242
- | Static value (string/object literal) | ~0ms | `context: 'staging'` |
11243
- | Inline expression (pure function) | ~0ms | `context: (event) => event.targetBranch` |
11244
- | Init job (impure function) | ~5-10s | `context: async (event) => await lookupEnv(event.targetBranch)` |
11422
+ | Value | Overhead | Example |
11423
+ | ------------------------ | --------- | ---------------------------------------- |
11424
+ | Static value | None | `context: 'staging'` |
11425
+ | Dynamic value (function) | Init step | `context: (event) => event.targetBranch` |
11426
+
11427
+ A static value is baked into the lock file and needs no init step. A dynamic value always resolves through the agent's init step, so reach for a function only when the value genuinely depends on the event.
11245
11428
 
11246
11429
  ## Tips
11247
11430
 
11248
- - **Write pure functions whenever possible** to avoid the init-job delay. Most context and env computations only need the event payload data.
11249
- - **Check compiler warnings** -- the compiler prints a `warning [W101]` when a function is classified as impure, naming the reason and the ~5-10s init-job cost. Run `kici preview` to see the injected `__init__` job listed under each affected job before your first run.
11250
- - **Runtime errors in inline expressions cause immediate job failure.** There is no fallback to the init-job path. If your pure function throws at runtime (e.g., accessing a property on `undefined`), the job fails immediately.
11251
- - **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where pure vs. impure functions run relative to rules, hooks, and step bodies.
11431
+ - **Prefer static values when you can.** Most context and env values are the same on every event; only make them dynamic when they truly depend on the event payload.
11432
+ - **Run `kici preview`** to see the injected `__init__` job listed under each affected job before your first run.
11433
+ - **A runtime error in a dynamic function fails the job.** If your function throws when the init step runs it (e.g., accessing a property on `undefined`), the job fails immediately.
11434
+ - **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where dynamic values run relative to rules, hooks, and step bodies.
11252
11435
  - **The event parameter is the normalized event envelope** — the same shape rules receive as `ctx.event`: `{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }` (see the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/) for the complete schema). Narrow on `event.type` (`'push'`, `'pull_request'`, `'tag'`, …) to branch per trigger kind. The raw provider webhook body is nested at `event.payload` (for GitHub pushes: `payload.ref`, `payload.after`, `payload.repository`, …).
11253
11436
 
11254
11437
  ---
@@ -11785,35 +11968,151 @@ export default workflow('org-lint', {
11785
11968
  });
11786
11969
  ```
11787
11970
 
11788
- Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org.
11971
+ Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org, including one whose identifier starts with a dot (`.github/workflows-config`) — a repo identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own. Path globs in `paths:` keep the usual convention and do not match dot-prefixed files unless the pattern spells the dot out.
11789
11972
 
11790
11973
  ### At a dual-repo checkout
11791
11974
 
11792
- The agent receives two sets of context during a global workflow execution:
11975
+ The agent checks out both repos. **Inside a step body**, `env` carries a pointer to each working tree:
11793
11976
 
11794
11977
  | `env` var | Points to |
11795
11978
  | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
11796
11979
  | `KICI_SOURCE_REPO_PATH` | The **source** repo's working tree (the repo that emitted the event). This is the repo the job's `$` / `git` commands operate on by default. |
11797
11980
  | `KICI_WORKFLOW_REPO_PATH` | The **workflow** repo's working tree (the repo that authored the workflow). Useful for reading shared scripts or config from your CI repo. |
11798
11981
 
11799
- Source repo secrets are **not** available to a global workflow's job by default see _Elevated access_ below.
11982
+ Both variables are step-body context. They are **not** projected into the job's process environment, so a job-level `env:` block, a container image's entrypoint, or a shell command outside a step body will not see them. Outside a step body, use the `sourceRepo` / `workflowRepo` pair on the filter, generator, and rule contexts described below.
11983
+
11984
+ They are also set **only when there are two repos to point at**. An event from the workflow's own repo is matched from that repo's lock file rather than as a global candidate, so the workflow runs as an ordinary single-repo workflow: one checkout, and neither variable set. Read them with a fallback, as the example above does.
11985
+
11986
+ A global workflow's job runs with **no secrets at all** — neither the source repo's nor its own. See _Secrets are not available_ below.
11987
+
11988
+ ### The triggering event
11989
+
11990
+ `ctx.event` inside a global workflow's job is the **source** repo's normalized event — the push or PR that fired the workflow, from a repo the workflow's own author may not own. `ctx.event.sourceRepo` names that repo.
11991
+
11992
+ That field is what makes a per-source-repo concurrency group expressible — and you have to write it. A global workflow runs on events from many repos, and their default branches share a name, so a group keyed on the branch alone puts every repo in one group, and with `cancelInProgress` (the default) one repo's push cancels another repo's in-flight run. That is still the behaviour of a branch-only group; naming the source repo in the key is what separates them:
11993
+
11994
+ ```ts
11995
+ concurrency: {
11996
+ group: ({ branch, event }) => `${event.sourceRepo}:${branch}`,
11997
+ cancelInProgress: true,
11998
+ },
11999
+ ```
12000
+
12001
+ ### Narrowing to the repos that need it
12002
+
12003
+ A global workflow that matches `myorg/*` will, by default, run on every repo in the org. Three mechanisms narrow it to the repos it actually applies to, in increasing order of power:
12004
+
12005
+ 1. **A `requires` content filter on the trigger** — the cheapest gate. The orchestrator checks a file's contents (a JSON-path probe over `package.json`, for example) and drops the workflow **before any agent is dispatched** when the condition is not met. See [`requires` on triggers](https://docs.kici.dev/user/sdk/triggers/#content-requirements-requires). This is provider-dependent — it needs a file-contents fetcher, which the GitHub provider supplies.
12006
+ 2. **A workflow-level `filter` predicate** — arbitrary TypeScript over the checked-out source tree (below). Works with any provider that clones.
12007
+ 3. **A `DynamicJobFn`** — generate the exact job set from the source repo's state ([Generating jobs per source repo](https://docs.kici.dev/user/global-workflows/#generating-jobs-per-source-repo) below).
12008
+
12009
+ ### Narrowing with a filter
12010
+
12011
+ Before reaching for a `filter`, check whether a declarative filter answers the question. `commitMessage` (on the trigger) and `requires` (over source files) are evaluated by the orchestrator from data it already has, so they cost no evaluation job at all — while a `filter` predicate dispatches one per (event × workflow repo). Gating on a `[skip ci]` marker, a conventional-commit prefix, or the contents of a named config file needs no predicate.
12012
+
12013
+ A workflow can declare a `filter`: a predicate that decides whether the workflow applies to this event at all.
12014
+
12015
+ ```ts
12016
+ import { workflow, job, step, push } from '@kici-dev/sdk';
12017
+
12018
+ export default workflow('org-container-lint', {
12019
+ on: [push({ repos: ['myorg/*'] })],
12020
+ filter: async ({ sourceRepo, changedFilesStatus, $ }) => {
12021
+ // `changedFiles` throws when the diff is unavailable, so guard first.
12022
+ if (changedFilesStatus !== 'fetched') return true;
12023
+ const found = await $`ls ${sourceRepo.path}`;
12024
+ return found.stdout.includes('Dockerfile');
12025
+ },
12026
+ jobs: [
12027
+ job('lint-dockerfile', {
12028
+ runsOn: ['kici:os:linux'],
12029
+ steps: [
12030
+ step('lint', async ({ $, env }) => $`hadolint ${env.KICI_SOURCE_REPO_PATH}/Dockerfile`),
12031
+ ],
12032
+ }),
12033
+ ],
12034
+ });
12035
+ ```
12036
+
12037
+ The filter receives a `FilterContext`:
12038
+
12039
+ | Property | Type | Description |
12040
+ | -------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------ |
12041
+ | `sourceRepo` | `RepoInfo` | The repo whose event triggered this evaluation, checked out on the evaluating agent. |
12042
+ | `workflowRepo` | `RepoInfo` | The repo that registered the workflow. Identical to `sourceRepo` for a same-repo workflow. |
12043
+ | `event` | `EventPayload` | The normalized event envelope. |
12044
+ | `changedFiles` | `string[]` | Files changed in this event. Throws when unavailable — guard with `changedFilesStatus`. |
12045
+ | `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` can be read. |
12046
+ | `env` | `Record<string, string\|undefined>` | Environment variables. |
12047
+ | `$` | zx shell | Shell executor. |
12048
+
12049
+ `RepoInfo` carries `path` (an absolute path to the checkout on the evaluating agent) plus optional `ref` and `sha`. **Both are optional** — an event that carries no single ref leaves them undefined, so guard before reading them.
12050
+
12051
+ **`sourceRepo.path` is not stable across evaluations.** Its _contents_ are: the evaluating agent and the later run see the same tree at the same commit. The path itself is not — a different working directory, and possibly a different machine. Read _through_ it; never embed it in a job name, an output, or anything compared across calls.
12052
+
12053
+ **A `filter` must be pure and deterministic.** Decide from the context alone — the event, the changed files, and the checked-out tree — so the same event always yields the same verdict.
12054
+
12055
+ ### Global and same-repo filters differ
12056
+
12057
+ The same `filter` keyword means two different things depending on whether the workflow is global:
12058
+
12059
+ | | Global workflow (`repos:` on a trigger) | Same-repo workflow |
12060
+ | ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
12061
+ | Evaluated | once per (event × workflow repo) | once per job that reaches dispatch, and once per job generator |
12062
+ | Evaluated relative to the run | **before** any run row exists | **after** the run row exists |
12063
+ | A `false` verdict leaves | no run at all — nothing appears in the dashboard | a run whose only entries are the evaluation jobs, rolling up to `success` |
12064
+ | `sourceRepo` vs `workflowRepo` | two different repos | the same repo |
12065
+
12066
+ Two consequences of the same-repo shape are worth designing for. A workflow with ten jobs calls its filter ten times for one event — each on its own agent with its own checkout and its own `$` — so anything the predicate does happens that many times: keep it cheap and side-effect free. And if the predicate can answer differently for the same event, the workflow will _partially_ dispatch, running some jobs and not others.
12067
+
12068
+ **A held or rejected job is not filtered at all.** A job held for approval, or rejected by a context rule, already has a gate — the hold or the rule — so it never takes a filter verdict, and an approved job dispatches without one. Concretely: a path filter cannot stop an approval request for a job the change does not concern.
12069
+
12070
+ ### Generating jobs per source repo
12071
+
12072
+ A global workflow's job generators run in the same pre-run evaluation as the filter, with both repos on disk. `sourceRepo` and `workflowRepo` are on the generator context, so one workflow repo can produce a different job set per source repo:
12073
+
12074
+ ```ts
12075
+ import { job, step, workflow, push, type DynamicJobFn } from '@kici-dev/sdk';
12076
+ import { readFile } from 'node:fs/promises';
12077
+
12078
+ const perRepoJobs: DynamicJobFn = async ({ sourceRepo }) => {
12079
+ if (!sourceRepo) return [];
12080
+ const pkg = JSON.parse(await readFile(`${sourceRepo.path}/package.json`, 'utf8'));
12081
+ return Object.keys(pkg.scripts ?? {})
12082
+ .filter((s) => s.startsWith('ci:'))
12083
+ .map((s) =>
12084
+ job(s.replace(':', '-'), {
12085
+ runsOn: ['kici:os:linux'],
12086
+ steps: [step('run', async ({ $ }) => $`pnpm ${s}`)],
12087
+ }),
12088
+ );
12089
+ };
12090
+
12091
+ export default workflow('org-ci', {
12092
+ on: [push({ repos: ['myorg/*'] })],
12093
+ jobs: [perRepoJobs],
12094
+ });
12095
+ ```
12096
+
12097
+ The same `sourceRepo.path` caution applies: read the tree through it, and derive job names from the repo's _contents_, never from the path.
11800
12098
 
11801
12099
  ## Enabling global workflows
11802
12100
 
11803
- Global workflows are **opt-in per org**. In a fresh org, `repos:`-bearing workflows are registered but never dispatched.
12101
+ Global workflows are gated by a **fleet-wide master switch** held by the orchestrator operator, off by default. Until it is on, `repos:`-bearing workflows are registered but never dispatched.
11804
12102
 
11805
- 1. Open the dashboard **Settings → Global workflows**.
11806
- 2. Turn on **Enable global workflows** (the master toggle). This is the kill-switch every other toggle below is ignored while this is off.
11807
- 3. Decide which authoring/source controls you need:
12103
+ 1. **The operator enables it cluster-wide** with `kici-admin cluster-settings set --global-workflows-enabled true`. This is the kill-switch — every per-org control below is ignored while it is off, and it cannot be flipped from the dashboard. The dashboard's **Settings → Global workflows** tab shows its current state as a read-only badge.
12104
+ 2. In the dashboard → **Settings Global workflows**, decide which authoring/source controls you need. These per-org lists stay dashboard-editable; an org that has set none means "no per-org restrictions", not a denial.
11808
12105
 
11809
- | Setting | What it controls | Typical use |
11810
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
11811
- | Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
11812
- | Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
11813
- | Elevated access | Authoring repos listed here get **read access to source-repo secrets** during execution. Globs matched against the authoring repo identifier. | A `myorg/ci-deploy` repo that needs to read a source repo's `NPM_TOKEN` to publish releases. |
12106
+ | Setting | What it controls | Typical use |
12107
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
12108
+ | Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
12109
+ | Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
12110
+ | Elevated access | **Deprecated and not enforced.** Stored and echoed back, but nothing reads it — a global workflow's job receives no secrets, so there is no access for it to grant. See _Secrets are not available_. | None. Clear the list so it does not imply a grant that is not in force. |
11814
12111
 
11815
12112
  All three lists accept globs. Leading `!` inside a single pattern is not supported here; negation is via the list-is-implicit-deny semantics, so keep it simple (`myorg/ci-*`, `myorg/platform-*`).
11816
12113
 
12114
+ Patterns match repo identifiers by the same rule as `repos:` on a trigger: an identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own and a wildcard segment matches one. `myorg/*` covers `myorg/.github`, and `**` covers every repo in the org. Review any existing entry that relies on a wildcard to reach — or to spare — a dot-prefixed repo name.
12115
+
11817
12116
  ### Saving and reverting
11818
12117
 
11819
12118
  The page is a two-state editor — changes are local until you click **Save changes**, and you can abandon them with **Discard changes**. There is no partial save; the PATCH is all-or-nothing per save click.
@@ -11825,17 +12124,23 @@ The page is a two-state editor — changes are local until you click **Save chan
11825
12124
  A global workflow fires only if:
11826
12125
 
11827
12126
  1. **The authoring repo is allowed.** If _Allowed author repos_ is ON, the workflow's authoring repo must match at least one allow-list glob. If OFF, any repo may author. Enforced at two points:
11828
- - At registration time (extraction from the lock file — non-matching globals are dropped with a warning).
12127
+ - At registration time (extraction from the lock file — non-matching globals are dropped, and the orchestrator logs `Global workflows excluded from registration` naming each one).
11829
12128
  - At dispatch time (defense-in-depth — policy changes after registration still take effect).
11830
12129
  2. **The source repo is not denied.** If the event's source repo matches any glob in _Blocked source repos_, the global workflow is skipped. Enforced at dispatch time.
11831
12130
 
11832
- Both checks are logged to the orchestrator. Grep the logs for `Skipping global workflow` to see enforcement in action.
12131
+ Both checks are logged to the orchestrator. Grep for `Global workflows excluded from registration` (registration time) and `Skipping global workflow dispatch` (dispatch time) to see enforcement in action.
12132
+
12133
+ Both checks read the settings of the organization the **event's source** resolves to. If no webhook source maps the event's routing key to an organization, the orchestrator resolves the built-in `__default__` organization anchor instead — and since nobody has enabled global workflows for that anchor, every global workflow is refused. The registration log line carries the organization it decided against plus the remedy, so this case is distinguishable from a real opt-in that is simply switched off. See the troubleshooting table below.
12134
+
12135
+ ### Secrets are not available
11833
12136
 
11834
- ### Elevated access (source-repo secrets)
12137
+ A global workflow's job is dispatched with **no secret material** — not the source repo's, and not the workflow repo's own. The organization-wide dispatch path binds no secret contexts, so a `contexts:` declaration on a global workflow resolves to nothing and any secret the steps expect is simply absent. Plan for it: a global workflow is for checks, policy and reporting that need only the two checkouts, not for deploys that need credentials.
11835
12138
 
11836
- By default a global workflow's job runs with credentials scoped to the **workflow** repo it can clone both repos but cannot read the source repo's scoped secrets. That's the safe default: a random workflow in `myorg/ci-pipelines` does not get read access to secrets in `myorg/backend` just because it runs on a push there.
12139
+ This is about your **stored secrets**, not about repository access: the job is still handed a short-lived clone token for each repo it checks out, which is how the dual checkout works at all. What it does not get is anything from a secret context.
11837
12140
 
11838
- Adding the authoring repo to the _Elevated access_ list flips that: the job receives the source repo's secret context, so deploy and release flows that need `NPM_TOKEN` / `AWS_ROLE_ARN` / etc. from the source repo can read them. Treat elevated repos as effective owners of every source repo's CI secrets — only add repos you fully trust.
12141
+ To run something that needs secrets on a source repo's event, put those jobs in a per-repository workflow in that repo, where the workflow's `contexts:` resolve normally.
12142
+
12143
+ The **Elevated access** setting reads as the way to lift this, and it is not: it is **deprecated and never consulted**. Nothing in the dispatch path reads the list, and adding a repo to it does not make any secret readable. It is kept only so an existing value stays visible and clearable, and is removed at the next major version — see [Deprecations](https://docs.kici.dev/user/deprecations/).
11839
12144
 
11840
12145
  ## When does it fire?
11841
12146
 
@@ -11843,14 +12148,111 @@ Same-repo globals (a workflow in `myorg/app` with `repos: ['myorg/app']`) fire o
11843
12148
 
11844
12149
  Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workflowRun()`, etc. all accept `repos:`. `kiciEvent()` / `schedule()` / cron-like triggers have no source repo, so they're always per-org-registered regardless of `repos:`.
11845
12150
 
12151
+ A global workflow that declares a `filter` or a job generator is decided by one **evaluation job per (event × workflow repo)**, dispatched before any run exists. That job checks out both repos once and evaluates every candidate workflow from that repo, so ten global workflows in one CI repo cost one evaluation, not ten.
12152
+
12153
+ When that evaluation cannot reach a verdict — it fails, breaches its budget, or never reports — the workflows it was deciding on **do not run**. On a provider that supports commit checks, that posts a `failure` check named **`KiCI: Organization workflow evaluation`** on the source commit, so the outcome is visible instead of silent. Three things to know about it:
12154
+
12155
+ - The check is posted whether the evaluation failed **outright** or only **partly**. A per-workflow budget breach, or a `filter` that throws, leaves that one workflow undecided while its neighbours from the same repo are decided and run normally; the check then names only the undecided ones. So a broken `filter` is reported the same way whether or not other global workflows happen to share its repo.
12156
+ - Branch protection that lists required checks by name is unaffected, because the check is not on that list. Merge automation that requires _every_ check to be green will block on it.
12157
+ - **The check clears only on a new commit.** A provider redelivery of the same event is dropped as a duplicate, so re-delivering the webhook will not re-run the evaluation.
12158
+
12159
+ ## Approval gates are not supported
12160
+
12161
+ A global workflow cannot carry an `approval` gate, at the workflow level or on a job. Approval holds are applied by the per-repository dispatch path; the global path dispatches its jobs without consulting one, so a gate declared here would never be enforced. `kici compile` refuses it with `error [E124]` rather than accepting a security control the workflow does not actually have. A job produced by a `dynamicJob` generator never passes through the compiler, so that case is caught at dispatch instead — the orchestrator logs an error naming the workflow and job, and runs it ungated.
12162
+
12163
+ To gate a deployment behind a human, put the gated jobs in a workflow whose triggers carry no `repos:`.
12164
+
12165
+ ## Re-running is not supported
12166
+
12167
+ A global workflow's run cannot be re-run. The rerun path resolves a workflow out of the repo the run acted on, and for a global workflow that is the **source** repo — not the workflow repo that declares it. Re-running is refused with an error naming both repos rather than resolving the wrong one, which for a source repo carrying a same-named workflow would silently run that workflow instead, with the source repo's credentials and none of the global job configuration.
12168
+
12169
+ To run it again, push a new commit to the source repo (a provider redelivery of the same event is dropped as a duplicate), or trigger it from the workflow repo.
12170
+
12171
+ ## Requirements a filter places on the run
12172
+
12173
+ A `filter` reads the source tree, so the evaluation must be able to obtain one. A job that restores its workflow source from the cache and has no source repository to clone from fails with an explicit error rather than evaluating the filter against an empty tree. This applies to dispatch paths that run without a source repository configured — a filter and such a path are mutually exclusive; drop one or the other.
12174
+
11846
12175
  ## Troubleshooting
11847
12176
 
11848
- | Symptom | Likely cause | Where to look |
11849
- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
11850
- | Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` / `Skipping global workflow registration: not permitted` |
11851
- | `repos:` has no effectworkflow only fires on its own repo | Master toggle OFF. Without opt-in, the orchestrator treats the workflow as per-repo-only. | Dashboard Settings Global workflows (top toggle) |
11852
- | Source repo secrets unavailable in a global job | Expected default elevate the authoring repo to grant access. | Dashboard → Settings → Global workflows _Elevated access_ |
11853
- | Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
12177
+ | Symptom | Likely cause | Where to look |
12178
+ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12179
+ | Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` (dispatch time) / `Global workflows excluded from registration` (registration time) |
12180
+ | A global workflow is never registered at all it is absent from `kici-admin registration list` | The push that should have registered it resolved to the `__default__` organization anchor, because no webhook source maps its routing key to an organization, or the fleet-wide master switch is off. | Orchestrator log: `Global workflows excluded from registration` with `"orgId": "__default__"`. Its `remedy` field names both fixes — map the source (`kici-admin source update <routingKey> --customer-id <org>`) and enable global workflows cluster-wide if it is not already (`kici-admin cluster-settings set --global-workflows-enabled true`). |
12181
+ | `repos:` has no effect workflow only fires on its own repo | The fleet-wide master switch is off. Without it, the orchestrator treats the workflow as per-repo-only. | Check the fleet-wide switch with `kici-admin cluster-settings show`. The dashboard → Settings → Global workflows tab shows it as a read-only badge. |
12182
+ | Secrets unavailable in a global job | Expected a global workflow's job receives no secrets at all, and the _Elevated access_ list is not enforced. | Move the jobs that need credentials into a per-repository workflow in the repo that owns the secrets |
12183
+ | Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
12184
+ | Global workflow registered, enabled, allowed — and still no run appears | Its `filter` returned `false`. A global filter runs before the run is created, so a suppressed workflow leaves nothing behind at all. | [Reading a global workflow's filter output](https://docs.kici.dev/user/global-workflows/#reading-a-global-workflows-filter-output) — the evaluation round's own log. The orchestrator also logs `Global workflow skipped by eval round`, naming the workflow and the reason. |
12185
+ | Global workflow never fires for one particular source repo | Its `repos:` patterns do not match that repo's identifier. | Orchestrator log: `Global workflows dropped by their repos filter` — one line per delivery, naming each dropped workflow, its repo and its patterns. |
12186
+ | A `failure` check named `KiCI: Organization workflow evaluation` on a commit | The pre-run evaluation failed or timed out, so the global workflows from that repo were not run. | Orchestrator log for the evaluation job; push a new commit to re-evaluate (a redelivery is dropped as a duplicate). |
12187
+ | Same-repo workflow shows a `success` run with no jobs in it | Its `filter` returned `false`. A same-repo filter runs after the run exists, so the run remains, carrying only the evaluation jobs. | The run detail page — the evaluation job's log records the filter verdict. |
12188
+ | Re-run is refused with "Cannot re-run an organization-wide workflow" | Expected — the run executed against a source repo that does not declare the workflow. | [Re-running is not supported](https://docs.kici.dev/user/global-workflows/#re-running-is-not-supported) — push a new commit to the source repo instead. |
12189
+ | Every global workflow stopped running right after an orchestrator upgrade | The agents were not upgraded first. An agent older than v0.5.0 cannot evaluate a global workflow, and one containing a `dynamicJob` now needs an evaluation even without a `filter` — so its **static** jobs stop too. | The `KiCI: Organization workflow evaluation` check names the agent versions it found. Upgrade every `kici:role:init-runner` agent to v0.5.0 or newer. |
12190
+
12191
+ ### Reading a global run in the dashboard
12192
+
12193
+ A global run is attributed to the **source** repo — the repo whose event
12194
+ triggered it, and whose code the jobs check out. Its run detail page names both
12195
+ repos, so you can tell it apart from an ordinary per-repo run:
12196
+
12197
+ | Row | Shows |
12198
+ | ------------ | ------------------------------------------------------------------------ |
12199
+ | `Repository` | the source repo — the one the run acted on |
12200
+ | `Defined in` | the workflow repo, tagged `Organization-wide`. Absent on an ordinary run |
12201
+ | `Workflow` | links into the **workflow** repo, on its default branch |
12202
+
12203
+ The `Workflow` link points at the workflow repo's default branch rather than at
12204
+ a commit: the run's own commit belongs to the source repo, and nothing records
12205
+ which commit of the workflow repo a given run used. So the link always shows the
12206
+ file as it stands now, which may have changed since the run.
12207
+
12208
+ The `Payload` tab shows the source repo's event — the webhook delivery the
12209
+ workflow reacted to, which for a global workflow comes from a repo you may not
12210
+ own. A global run dispatched before your orchestrator stored payloads for this
12211
+ path has none, and its tab reports that it could not load one.
12212
+
12213
+ #### Who can see it
12214
+
12215
+ A global run belongs to **both** repos, so a member whose role is scoped to
12216
+ either one reaches it — the team whose push triggered it, and the team that
12217
+ authored the workflow. Both see it in the run list, in the repository filter
12218
+ (which offers both names), and on the run detail page. Cancelling follows the
12219
+ same rule, so the team whose workflow is running can always stop it.
12220
+
12221
+ Releasing a **held** run is the one exception: approving a hold permits code to
12222
+ run against the source repo, so it stays with a member scoped to that repo. A
12223
+ member scoped only to the workflow repo sees the run but not its hold.
12224
+
12225
+ This applies only where the two repos genuinely differ. An ordinary per-repo run
12226
+ records no separate workflow repo and is scoped to its own repo exactly as
12227
+ before, and a member scoped to neither repo sees nothing in either case.
12228
+
12229
+ ### Reading a global workflow's filter output
12230
+
12231
+ A global workflow's `filter` runs in a pre-run evaluation round, and that round
12232
+ decides whether a run exists at all — so on the path where it suppresses a
12233
+ workflow there is no run, and nothing appears in the dashboard. The round's own
12234
+ log is still recorded. Read it with the orchestrator admin CLI, in two steps:
12235
+
12236
+ ```bash
12237
+ # 1. Find the round. Its workflow name is __globaleval__<owner>/<repo> of the
12238
+ # WORKFLOW repo. In the JSON rows, `id` is the job id and `run_id` is the
12239
+ # run id.
12240
+ kici-admin queue list --workflow-name '__globaleval__myorg/ci-pipelines' --limit 5 --json
12241
+
12242
+ # 2. Print the round's log (step 0 is the evaluation itself).
12243
+ kici-admin runs logs <run_id> --job <id>
12244
+ ```
12245
+
12246
+ Use `--json` on the first command: the plain table abbreviates both ids to their
12247
+ first eight characters, and the second command needs them in full.
12248
+
12249
+ The two steps need different permissions, so run both with an **owner or admin**
12250
+ token. Step 1 reads the dispatch queue, which requires `secret.read` — an auditor
12251
+ token is refused with a 403 and never reaches step 2. Step 2 requires only
12252
+ `run.read`, which every role carries.
12253
+
12254
+ Anything your `filter` writes with `console.log` appears there, alongside the
12255
+ per-candidate verdicts the round recorded.
11854
12256
 
11855
12257
  ## See also
11856
12258
 
@@ -12036,7 +12438,7 @@ export default workflow('build', {
12036
12438
 
12037
12439
  Per-field rules:
12038
12440
 
12039
- - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/kici-admin-cli#allow-http-npm--permit-non-https-private-npm-registries)).
12441
+ - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#allow-http-npm--permit-non-https-private-npm-registries)).
12040
12442
  - **`scope`** — Optional. When present, the registry serves only that scope (`@my-org`). When absent, this entry becomes the **default** registry — at most one entry may omit `scope`.
12041
12443
  - **`tokenSecret`** — Mandatory `<context>:<secret-name>`. The orchestrator looks up the secret in the named context via the per-context secret resolver. The bare name **must not** contain a colon.
12042
12444
  - **`alwaysAuth`** — Defaults to `true`. Forces npm to send the token on every request (even GETs), which is what most managed-registry providers require.
@@ -12250,7 +12652,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
12250
12652
 
12251
12653
  - [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<context>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
12252
12654
  - [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
12253
- - [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/kici-admin-cli#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
12655
+ - [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
12254
12656
 
12255
12657
  ---
12256
12658
 
@@ -12556,7 +12958,7 @@ KiCI provides an explicit secrets API that gives workflow steps controlled acces
12556
12958
 
12557
12959
  ## Overview
12558
12960
 
12559
- Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
12961
+ Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration/) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
12560
12962
 
12561
12963
  This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
12562
12964
 
@@ -12577,13 +12979,13 @@ Use whichever fits the workflow — most small teams stay on the dashboard; ops
12577
12979
 
12578
12980
  ### When the operator has disabled dashboard writes
12579
12981
 
12580
- The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy). When that flip is on:
12982
+ The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy/). When that flip is on:
12581
12983
 
12582
12984
  - The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Hovering or keyboard-focusing the lock shows a tooltip with the exact `kici-admin secret set` invocation needed. The control itself is inert, so there is nothing to click.
12583
12985
  - The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
12584
12986
  - `kici-admin secret set` becomes the single entry point for new and updated secret values.
12585
12987
 
12586
- This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, environment bindings).
12988
+ This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, context bindings).
12587
12989
 
12588
12990
  ### CLI input modes
12589
12991
 
@@ -12618,7 +13020,7 @@ Two cross-cutting flags help every mode:
12618
13020
 
12619
13021
  `kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
12620
13022
 
12621
- A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy#cli-input-modes-for-the-plaintext-path).
13023
+ A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy/#cli-input-modes-for-the-plaintext-path).
12622
13024
 
12623
13025
  ## Accessing secrets
12624
13026
 
@@ -12853,7 +13255,7 @@ Note that `get()` is async -- you must `await` the result.
12853
13255
 
12854
13256
  When you run `kici types`, the compiler generates a `.kici/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
12855
13257
 
12856
- See [CLI reference](https://docs.kici.dev/user/cli) for the `kici types` command.
13258
+ See [CLI reference](https://docs.kici.dev/user/cli/authoring-and-local/#kici-types) for the `kici types` command.
12857
13259
 
12858
13260
  ---
12859
13261
 
@@ -13169,9 +13571,8 @@ when you want a list entry pinned to this specific App rather than
13169
13571
  applying to any source in the org:
13170
13572
 
13171
13573
  ```bash
13172
- # Enable global workflows for the org
13173
- kici-admin org-settings global-workflows set-enabled true \
13174
- --customer-id <orgId>
13574
+ # Enable global workflows cluster-wide (the master switch is fleet-wide, held by the operator)
13575
+ kici-admin cluster-settings set --global-workflows-enabled true
13175
13576
 
13176
13577
  # Allow the listed repo as an author for any source in the org
13177
13578
  kici-admin org-settings global-workflows allow-add 'my-org/ci-workflows/*' \
@@ -13606,9 +14007,8 @@ org-scoped (one row per `customer_id`); each list entry can optionally
13606
14007
  pin to a specific source via `--source <routingKey>`:
13607
14008
 
13608
14009
  ```bash
13609
- # Enable global workflows for the org
13610
- kici-admin org-settings global-workflows set-enabled true \
13611
- --customer-id <orgId>
14010
+ # Enable global workflows cluster-wide (the master switch is fleet-wide, held by the operator)
14011
+ kici-admin cluster-settings set --global-workflows-enabled true
13612
14012
 
13613
14013
  # Allow authors from any source in the org
13614
14014
  kici-admin org-settings global-workflows allow-add \
@@ -13983,12 +14383,13 @@ The `ProviderRegistry` maps routing keys to provider bundles. Each routing key (
13983
14383
  - `WebhookNormalizer` (required) -- normalizes incoming webhooks to a standard format
13984
14384
  - `LockFileFetcher` -- fetches lock files from the repository
13985
14385
  - `ChangedFilesFetcher` -- determines which files changed
14386
+ - `FileContentsFetcher` -- reads arbitrary repository files at a ref, for the declarative content-requirements (`requires`) filter
13986
14387
  - `CloneTokenProvider` -- generates clone tokens for agents
13987
14388
  - `RepoUrlBuilder` -- builds clone URLs and raw file URLs
13988
14389
  - `ContributorResolver` -- resolves contributor permissions for trust-tier gating
13989
14390
  - `CheckStatusPoster` -- posts check statuses (approval/hold) to the git provider
13990
14391
 
13991
- A GitHub App source populates all seven. A plain generic webhook source carries only the normalizer -- it has no repository API to fetch a lock file, resolve a contributor, or post a check against -- so the pipeline skips the stages whose interface is absent rather than failing the delivery.
14392
+ A GitHub App source populates all eight -- though the file-contents capability arrives as a per-delivery factory rather than a prebuilt instance, because a GitHub client is scoped to one installation and the installation id is only known once the delivery's credentials are resolved. A plain generic webhook source carries only the normalizer -- it has no repository API to fetch a lock file, resolve a contributor, or post a check against -- so the pipeline skips the stages whose interface is absent rather than failing the delivery.
13992
14393
 
13993
14394
  Provider registrations are managed via the `sources` database table, not via `SharedConfig`. When the orchestrator connects to the Platform relay, it reads source records from the DB and sends `source.register` messages. Changes to sources (add/remove) are detected via PostgreSQL LISTEN/NOTIFY on the `sources_change` channel and pushed to the Platform via `source.secrets` and `source.register`/`source.deregister`.
13994
14395
 
@@ -14076,10 +14477,11 @@ GitHub --> Platform Relay --> Orchestrator --> Agent
14076
14477
  12. **Orchestrator extracts registrations** on default-branch pushes: persists registerable workflows (event, schedule, lifecycle triggers) for cluster-wide event matching.
14077
14478
  13. **Orchestrator notifies the event router** on default-branch pushes: after the registrations are persisted, emits a `registration.updated` event via `eventRouter.emit()` (if event routing is active). Workflow event subscriptions are the persisted registrations themselves, matched at emit time through the registration index.
14078
14479
  14. **Orchestrator fetches changed files** via the provider's `ChangedFilesFetcher` for path-based trigger filtering (skipped when no workflow uses path filters).
14079
- 15. **Orchestrator matches triggers** against lock file using `matchAllWorkflows()` from `@kici-dev/engine`.
14080
- 16. **Orchestrator checks caches** for source tarballs and dependency tarballs.
14081
- 17. **Orchestrator dispatches jobs** to agents via the job queue and WebSocket.
14082
- 18. **Orchestrator persists a delivery row** keyed by `(org_id, delivery_id)` to its own `event_log`, including a pointer to the gzipped payload in object storage. The orchestrator's delivery log is surfaced in the dashboard's Settings → Event log tab. See [`webhook-delivery.md`](https://docs.kici.dev/architecture/webhooks/webhook-delivery/#delivery-log).
14480
+ 15. **Orchestrator matches triggers** against the lock file using `matchWorkflowsForEvent()` from `@kici-dev/engine` -- an event-type-bucketed candidate scan that evaluates only the workflows subscribed to this event type. (The single-registration global / cross-source paths evaluate one lock entry at a time via `matchAllWorkflows()`.)
14481
+ 16. **Orchestrator applies the content-requirements filter** to the matched candidates: for each trigger that declares `requires`, it reads the named source files at the event's ref through the provider's `FileContentsFetcher` (once per distinct `(repo, sha, path)` via an LRU cache) and evaluates the declarative requirement. Candidates that fail -- or that cannot be evaluated at all (unreadable or oversize content, a fetch error, no fetcher wired) -- are dropped before dispatch with the concrete reason logged. No workflow code runs at this stage. Skipped entirely when no matched trigger declares `requires`.
14482
+ 17. **Orchestrator checks caches** for source tarballs and dependency tarballs.
14483
+ 18. **Orchestrator dispatches jobs** to agents via the job queue and WebSocket.
14484
+ 19. **Orchestrator persists a delivery row** keyed by `(org_id, delivery_id)` to its own `event_log`, including a pointer to the gzipped payload in object storage. The orchestrator's delivery log is surfaced in the dashboard's Settings → Event log tab. See [`webhook-delivery.md`](https://docs.kici.dev/architecture/webhooks/webhook-delivery/#delivery-log).
14083
14485
 
14084
14486
  ## Job execution flow
14085
14487
 
@@ -14195,7 +14597,7 @@ Execution Job Dispatch --> Execution Agent
14195
14597
  | |
14196
14598
  | |-- Download source tarball (sourceTarUrl) -> extract to workDir/.kici/
14197
14599
  | |-- Download deps tarball (depsUrl) -> verify SHA-256 -> extract to .kici/node_modules/
14198
- | |-- Register @kici-dev/shared/ts-loader-hook
14600
+ | |-- Register @kici-dev/core/ts-loader-hook
14199
14601
  | |-- Verify workflow contentHash against lock file (drift guard)
14200
14602
  | |-- Dynamic-import workflow .ts
14201
14603
  | |-- Execute steps
@@ -14254,7 +14656,7 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
14254
14656
 
14255
14657
  ### Cross-source / no-contentHash workflows
14256
14658
 
14257
- - **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 32.
14659
+ - **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 34.
14258
14660
  - **Cross-source / global-workflow dispatch** (a workflow registered against source A fired by a webhook on source B) bypasses both caches. The registration's lock file entry still carries `contentHash`, but the cross-source path always clone-and-installs — the eval temp dir doesn't ship `@kici-dev/sdk`. The execution agent still verifies `contentHash` against the cloned source for drift detection.
14259
14661
 
14260
14662
  ### Build deduplication
@@ -14499,7 +14901,7 @@ Agent Orchestrator
14499
14901
 
14500
14902
  ### Registration extraction flow
14501
14903
 
14502
- When code is pushed to the default branch, the orchestrator extracts event-triggered workflows from the lock file and stores them as registrations for cluster-wide event matching.
14904
+ When code is pushed to the default branch, the orchestrator extracts registerable workflows from the lock file and stores them as registrations for cluster-wide event matching. Non-Git triggers (`kici_event`, `schedule`, `generic_webhook`, …) live there because they have no per-repo lock-file pipeline to fall back on; Git-provider triggers (`push`, `pr`, `tag`, …) are indexed too, so the cross-source dispatch path can resolve them by `(customer_id, repo_identifier)` when a generic webhook targets an externally-hosted repo. For same-source Git events the per-event lock-file pipeline remains the primary matcher — registration is an additive index.
14503
14905
 
14504
14906
  ```
14505
14907
  Git Push to Default Branch
@@ -14512,8 +14914,15 @@ GitHub Webhook -> Platform Relay -> Orchestrator Processor
14512
14914
  |-- extractRegisterableWorkflows(fullLockFile)
14513
14915
  | |-- For each workflow entry in lock file:
14514
14916
  | | Check if any trigger type is registerable
14515
- | | (kici_event, workflow_complete, job_complete,
14516
- | | generic_webhook, schedule, lifecycle)
14917
+ | | (the RegisterableTriggerType enum — the non-Git
14918
+ | | set kici_event, workflow_complete,
14919
+ | | workflows_failed_batch, job_complete,
14920
+ | | generic_webhook, schedule, lifecycle, webhook,
14921
+ | | plus every Git-provider trigger: push, pr, tag,
14922
+ | | comment, review, review_comment, release,
14923
+ | | dispatch, create, delete, status, workflow_run,
14924
+ | | fork, star, watch)
14925
+ | | ... or the workflow has repo patterns (global workflow)
14517
14926
  | |-- Return array of registerable workflows
14518
14927
  |
14519
14928
  |-- globalWorkflowPolicy.isWorkflowRepoAllowed() (if policy configured)
@@ -14891,7 +15300,7 @@ The Platform tier exposes a `/ws/browser` WebSocket endpoint for dashboard clien
14891
15300
  - [Architecture overview](https://docs.kici.dev/architecture/overview/) -- three-tier model and component responsibilities
14892
15301
  - [Protocol messages](https://docs.kici.dev/architecture/protocol-messages/) -- WebSocket message schemas
14893
15302
  - [Event system internals](https://docs.kici.dev/architecture/webhooks/event-system/) -- event router, registration model, cron scheduler
14894
- - [Execution lifecycle](https://docs.kici.dev/architecture/execution/state-machine/) -- run, job, and step status vocabularies and terminal states
15303
+ - [Execution status vocabulary](https://docs.kici.dev/architecture/execution/state-machine/) -- run, job, and step status vocabularies and terminal states
14895
15304
  - [Webhook delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/) -- detailed webhook processing pipeline
14896
15305
  - [Operator: dependency caching](https://docs.kici.dev/operator/dependency-caching/) -- configuration guide
14897
15306
  - [Operator: monitoring & tracing](https://docs.kici.dev/operator/observability/monitoring/) -- trace fields and Loki queries
@@ -14990,7 +15399,7 @@ The agent is the execution worker. It runs on customer infrastructure and has fu
14990
15399
  Shared business logic used by all three tiers. Single source of truth for cross-tier concerns. Has no internal `@kici-dev/*` dependencies -- only a handful of third-party libraries.
14991
15400
 
14992
15401
  - Protocol message schemas (Zod-based, direction-specific unions including dashboard REST-over-WS, browser live streaming, the test-relay control plane, log pull, run events, peer-to-peer, cluster join, and source registration)
14993
- - Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster)
15402
+ - Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, FileContentsFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster)
14994
15403
  - Trigger matching engine (branch, path, event evaluation)
14995
15404
  - Dispatch inputs (input descriptors, extraction from the trigger event, and coercion to typed values)
14996
15405
  - Matrix expansion and fanout (combination expansion with include/exclude, job-name suffix formatting, and materialization of one matrix or multi-host job into N dispatchable children)
@@ -15006,11 +15415,16 @@ Shared business logic used by all three tiers. Single source of truth for cross-
15006
15415
  - Build provenance (in-toto statement schema, DSSE envelope, attestation bundle, verification)
15007
15416
  - Artifact name contract (the shared filesystem/URL-safe name schema the orchestrator, agent, and SDK all validate against)
15008
15417
  - Developer MCP tool schemas (argument schemas for the AI-agent tool surface)
15418
+ - Developer-operations contract (one row per workflow-developer operation declaring which entrypoints expose it -- the shared REST API behind the web UI and the `kici` CLI, the AI-agent tool surface, and a curated UI flag -- asserted against each real surface by congruence tests)
15009
15419
  - Label utilities (platform label derivation, runsOn normalization, `kici:*` set-only reserved namespace, role labels)
15010
15420
  - Host inventory (the canonical queryable host-roster schema shared by the orchestrator's roster store, the agent-facing inventory API, and the SDK's `ctx.kici.inventory`)
15011
15421
  - Audit policy and retention (per-action access-log sampling, warm-retention windows for cold-store eligibility, federated activity row schema)
15012
15422
  - Scaler backend type enum (`container`, `bare-metal`, `firecracker`, `kubernetes`)
15013
15423
  - Registration trigger type enum (registerable trigger discriminator)
15424
+ - Sandbox capability set (the Linux capability names a container sandbox may add or drop, shared by the SDK validator, the compiler, and the dispatch resolver)
15425
+ - Plan tier vocabulary (the hosted plan tiers and the purchasable subset, shared by the Platform and the browser dashboard)
15426
+ - Infrastructure alert vocabulary (the diagnostics alert types and severities the Platform mints and the dashboard and `kici` CLI render)
15427
+ - Metric catalog (the generated Prometheus metric inventory, its naming policy, and metric-kind compatibility checks)
15014
15428
  - Bundler config (shared bundler configuration consumed by `e2e/helpers/service-deploy.ts`; the agent runtime uses the `@kici-dev/core/ts-loader-hook` to transform TypeScript on import, with no runtime bundler step)
15015
15429
 
15016
15430
  > Source: `packages/engine/src/`
@@ -15134,7 +15548,7 @@ KiCI uses application-level tenant isolation. The Platform dashboard API accepts
15134
15548
  ## See also
15135
15549
 
15136
15550
  - [Multi-Orchestrator Architecture](https://docs.kici.dev/architecture/clustering/multi-orchestrator/) -- P2P clustering, Raft consensus, job rerouting
15137
- - [Execution lifecycle](https://docs.kici.dev/architecture/execution/state-machine/) -- run, job, and step status vocabularies and the tracker that owns lifecycle state
15551
+ - [Execution status vocabulary](https://docs.kici.dev/architecture/execution/state-machine/) -- run, job, and step status vocabularies and the tracker that owns lifecycle state
15138
15552
  - [Protocol Messages](https://docs.kici.dev/architecture/protocol-messages/) -- WebSocket message schemas for all three layers
15139
15553
  - [Webhook Delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/) -- end-to-end trace of a webhook through all three tiers
15140
15554