@kici-dev/compiler 0.1.16 → 0.1.17

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 (136) hide show
  1. package/dist/auth/headless-detect.js +1 -1
  2. package/dist/cli-banner.js +1 -1
  3. package/dist/cli.js +56 -19
  4. package/dist/commands/approve.js +1 -1
  5. package/dist/commands/compile.js +5 -3
  6. package/dist/commands/diagnostics.d.ts +7 -0
  7. package/dist/commands/diagnostics.js +58 -0
  8. package/dist/commands/docs.js +1 -1
  9. package/dist/commands/drain-worker.js +1 -1
  10. package/dist/commands/endpoints.js +1 -1
  11. package/dist/commands/fixture.js +1 -1
  12. package/dist/commands/held-run-client.js +1 -1
  13. package/dist/commands/held-run-resolve.js +1 -1
  14. package/dist/commands/hook.js +1 -1
  15. package/dist/commands/index.d.ts +16 -4
  16. package/dist/commands/index.js +10 -4
  17. package/dist/commands/init.js +2 -2
  18. package/dist/commands/login.d.ts +0 -2
  19. package/dist/commands/login.js +1 -3
  20. package/dist/commands/logout.js +1 -1
  21. package/dist/commands/orchestrators.d.ts +25 -0
  22. package/dist/commands/orchestrators.js +115 -0
  23. package/dist/commands/org.js +1 -1
  24. package/dist/commands/reject.js +1 -1
  25. package/dist/commands/run.d.ts +2 -9
  26. package/dist/commands/run.js +256 -315
  27. package/dist/commands/runs/cancel.d.ts +6 -0
  28. package/dist/commands/runs/cancel.js +32 -0
  29. package/dist/commands/runs/list.d.ts +6 -0
  30. package/dist/commands/runs/list.js +49 -0
  31. package/dist/commands/runs/logs.d.ts +7 -0
  32. package/dist/commands/runs/logs.js +67 -0
  33. package/dist/commands/runs/rerun.d.ts +5 -0
  34. package/dist/commands/runs/rerun.js +21 -0
  35. package/dist/commands/runs/show.d.ts +5 -0
  36. package/dist/commands/runs/show.js +65 -0
  37. package/dist/commands/secrets-list.d.ts +7 -9
  38. package/dist/commands/secrets-list.js +40 -64
  39. package/dist/commands/test.d.ts +4 -0
  40. package/dist/commands/test.js +1 -1
  41. package/dist/commands/types.d.ts +3 -5
  42. package/dist/commands/types.js +13 -37
  43. package/dist/commands/verify-attestation.d.ts +12 -0
  44. package/dist/commands/verify-attestation.js +69 -0
  45. package/dist/commands/watch.js +1 -1
  46. package/dist/commands/workflows.js +14 -34
  47. package/dist/errors/capability-gap.js +1 -1
  48. package/dist/errors/formatter.js +1 -1
  49. package/dist/errors/index.js +1 -1
  50. package/dist/execution/executor.js +1 -1
  51. package/dist/execution/index.js +1 -1
  52. package/dist/execution/sdk-alias.js +1 -1
  53. package/dist/execution/ts-loader.js +1 -1
  54. package/dist/fixtures/compiler.js +1 -1
  55. package/dist/fixtures/defaults/index.js +1 -1
  56. package/dist/format.js +1 -1
  57. package/dist/generators/secrets-dts.js +2 -2
  58. package/dist/hooks/detector.js +1 -1
  59. package/dist/hooks/index.js +1 -1
  60. package/dist/hooks/installer.js +1 -1
  61. package/dist/hooks/templates.js +1 -1
  62. package/dist/index.js +1 -1
  63. package/dist/llm-context/llms-full.txt +1047 -132
  64. package/dist/llm-context/llms.txt +4 -1
  65. package/dist/local-executor/dag-scheduler.js +1 -1
  66. package/dist/local-executor/index.js +1 -1
  67. package/dist/local-executor/job-runner.js +37 -16
  68. package/dist/local-executor/materializer.js +1 -1
  69. package/dist/local-executor/output-streamer.js +1 -1
  70. package/dist/local-executor/payload-generator.js +1 -1
  71. package/dist/local-executor/picker.js +1 -1
  72. package/dist/local-executor/secret-loader.js +1 -1
  73. package/dist/local-executor/to-event-payload.js +1 -1
  74. package/dist/local-executor/types.js +1 -1
  75. package/dist/local-executor/workflow-lock.js +0 -0
  76. package/dist/lockfile/generator.d.ts +7 -5
  77. package/dist/lockfile/generator.js +21 -18
  78. package/dist/lockfile/hash-files.js +1 -1
  79. package/dist/lockfile/hasher.js +1 -1
  80. package/dist/lockfile/index.js +1 -1
  81. package/dist/lockfile/purity-analyzer.js +1 -1
  82. package/dist/postinstall.js +1 -1
  83. package/dist/provenance-trust-root.d.ts +14 -0
  84. package/dist/provenance-trust-root.js +49 -0
  85. package/dist/remote/config.d.ts +6 -2
  86. package/dist/remote/config.js +7 -4
  87. package/dist/remote/dashboard-client.d.ts +92 -0
  88. package/dist/remote/dashboard-client.js +168 -0
  89. package/dist/remote/encryption.js +3 -4
  90. package/dist/remote/history.js +1 -1
  91. package/dist/remote/oauth.js +1 -1
  92. package/dist/remote/oidc-discovery.js +1 -1
  93. package/dist/remote/output/json.js +1 -1
  94. package/dist/remote/output/junit.js +1 -1
  95. package/dist/remote/output/streaming.js +1 -1
  96. package/dist/remote/output/summary.js +1 -1
  97. package/dist/remote/platform-client.d.ts +142 -0
  98. package/dist/remote/platform-client.js +187 -0
  99. package/dist/remote/prod-defaults.js +1 -1
  100. package/dist/remote/render.d.ts +11 -0
  101. package/dist/remote/render.js +48 -0
  102. package/dist/remote/secret-upload.js +1 -1
  103. package/dist/remote/uploader.d.ts +9 -0
  104. package/dist/remote/uploader.js +51 -4
  105. package/dist/templates/agents-md.d.ts +1 -1
  106. package/dist/templates/agents-md.js +8 -3
  107. package/dist/templates/index.js +1 -1
  108. package/dist/templates/package-json.js +4 -5
  109. package/dist/templates/tsconfig-json.js +1 -1
  110. package/dist/templates/workflows/hello-world.js +2 -2
  111. package/dist/templates/workflows/hello-world.ts +1 -1
  112. package/dist/templates/workflows/pr-checks.js +3 -3
  113. package/dist/templates/workflows/pr-checks.ts +2 -2
  114. package/dist/test-runner/dry-run.js +1 -1
  115. package/dist/test-runner/event-types.js +1 -1
  116. package/dist/test-runner/git-detector.js +1 -1
  117. package/dist/test-runner/index.js +1 -1
  118. package/dist/test-runner/job-executor.js +1 -1
  119. package/dist/test-runner/output-formatter.js +1 -1
  120. package/dist/test-runner/payload-builder.js +1 -1
  121. package/dist/test-runner/rule-evaluator.js +1 -1
  122. package/dist/test-runner/secrets-file.js +1 -1
  123. package/dist/test-runner/step-context.js +10 -6
  124. package/dist/types.d.ts +8 -5
  125. package/dist/types.js +2 -1
  126. package/dist/validation/index.js +1 -1
  127. package/dist/validation/validator.js +1 -1
  128. package/dist/workflows/hello-world.ts +1 -1
  129. package/dist/workflows/pr-checks.ts +2 -2
  130. package/package.json +5 -4
  131. package/sbom.spdx.json +68 -38
  132. package/dist/commands/cancel.js +0 -124
  133. package/dist/commands/status.js +0 -212
  134. package/dist/remote/client.js +0 -203
  135. package/dist/remote/observer.js +0 -174
  136. /package/dist/{chunk-gOLHoazu.js → chunk-BTugEXQM.js} +0 -0
@@ -26,7 +26,7 @@ Why the lock file must stay in sync with workflow source, how to commit both tog
26
26
 
27
27
  ### [CLI reference](cli-reference.md)
28
28
 
29
- All CLI commands provided by `@kici-dev/compiler`: `kici compile` (with watch mode and check mode), `kici run` (local and remote execution), `kici test` (event simulation with dry-run, filtering, debug output, and custom payloads), `kici login`/`logout`/`org` (authentication and org management), `kici status`/`cancel` (run management), `kici secrets` (secret listing), `kici types` (type generation), `kici fixture` (generate test payloads), `kici init` (interactive project scaffolding), `kici hook` (pre-commit hook installation), `kici endpoints` (webhook entrypoints), and `kici workflows` (workflow listing). Includes environment variables and exit codes.
29
+ All CLI commands provided by `@kici-dev/compiler`: `kici compile` (with watch mode and check mode), `kici run` (local and remote execution), `kici test` (event simulation with dry-run, filtering, debug output, and custom payloads), `kici login`/`logout`/`org` (authentication and org management), `kici diagnostics` (infrastructure tree) and `kici runs` (`list`/`show`/`logs`/`rerun`/`cancel` run management), `kici secrets` (secret listing), `kici types` (type generation), `kici fixture` (generate test payloads), `kici init` (interactive project scaffolding), `kici hook` (pre-commit hook installation), `kici endpoints` (webhook entrypoints), and `kici workflows` (workflow listing). Includes environment variables and exit codes.
30
30
 
31
31
  ### [Workflow patterns](workflow-patterns.md)
32
32
 
@@ -215,6 +215,8 @@ This workflow:
215
215
  - Runs a `lint` job first
216
216
  - Runs a `test` job after lint succeeds (`needs: [lint]`)
217
217
 
218
+ `runsOn` selects which agents may run a job. Every agent self-reports `kici:os:<platform>`, `kici:arch:<cpu>`, and `kici:host:<hostname>`, so `runsOn: 'kici:os:linux'` targets any connected Linux agent with zero configuration — `kici init` scaffolds workflows with exactly that. Use a custom label such as `'linux'` or `'gpu'` (defined in your scaler's `labelSet`) to target a specific pool instead. See the [runsOn forms](./sdk/core.md#runson-forms) reference for the full label model.
219
+
218
220
  **Single-step shortcut.** If a job only has one step, pass `run` directly to `job()` instead of building a `steps: [step(...)]` array:
219
221
 
220
222
  ```typescript
@@ -310,7 +312,7 @@ This updates `.kici/package.json` and generates (or updates) `package-lock.json`
310
312
 
311
313
  ### Dependency resolution contract
312
314
 
313
- Every `.kici/` dependency must be resolvable from the **single cloned repository**. When a job runs, the agent clones only this repository and installs `.kici/` dependencies with your repo's package manager (npm or pnpm; yarn is not supported the agent rejects it with an actionable error). A dependency that points outside the cloned repo cannot be resolved.
315
+ Every `.kici/` dependency must be resolvable from the **single cloned repository**. When a job runs, the agent clones only this repository and installs `.kici/` dependencies with your repo's package manager npm, pnpm, or yarn classic (v1). yarn berry (v2+) is not yet supported. A dependency that points outside the cloned repo cannot be resolved.
314
316
 
315
317
  In practice:
316
318
 
@@ -416,6 +418,8 @@ Or add the flag to your root `package.json`:
416
418
 
417
419
  ## Authoring KiCI workflows with LLM coding agents
418
420
 
421
+ KiCI is LLM-ready by design. Because workflows are real, typed TypeScript, coding agents reason over the SDK's `.d.ts` signatures instead of guessing a bespoke YAML DSL — and they verify their own pipelines with the same `kici test` and `kici run local` loop you use, so there's no push-to-find-out round-trip. First-class agent context ships in the box, so an agent is briefed the moment it opens the project.
422
+
419
423
  KiCI ships first-class context for LLM coding agents (Claude Code, Cursor, Aider, etc.). When you scaffold a project with `kici init`, the CLI writes `.kici/AGENTS.md`, a one-page briefing that tells the agent:
420
424
 
421
425
  - where the SDK type declarations live (`node_modules/@kici-dev/sdk/dist/index.d.ts`)
@@ -1888,33 +1892,33 @@ function job(options: JobOptions): Job;
1888
1892
 
1889
1893
  **Parameters:**
1890
1894
 
1891
- | Parameter | Type | Required | Description |
1892
- | -------------------------- | --------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
1893
- | `name` | `string` | no | Job name (auto-generated UUID if omitted) |
1894
- | `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
1895
- | `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
1896
- | `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](#single-step-job-shorthand). Mutually exclusive with `steps`. |
1897
- | `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](#job-dependencies-needs) |
1898
- | `options.rules` | `Rule[]` | no | Conditions for conditional execution |
1899
- | `options.description` | `string` | no | Human-readable description |
1900
- | `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
1901
- | `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
1902
- | `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
1903
- | `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
1904
- | `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. |
1905
- | `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
1906
- | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
1907
- | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](../concurrency.md). |
1908
- | `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
1909
- | `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
1910
- | `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
1911
- | `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
1912
- | `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
1913
- | `options.afterStep` | `HookInput` | no | Hook that runs after each step |
1914
- | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](../hooks.md#hook-timeout). |
1915
- | `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](#timeouts). |
1916
- | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](#per-job-resources) below. |
1917
- | `options.init` | `GenericInitConfig \| GenericInitConfig[] \| false` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. See [Per-job init](#per-job-init) below. |
1895
+ | Parameter | Type | Required | Description |
1896
+ | -------------------------- | --------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1897
+ | `name` | `string` | no | Job name (auto-generated UUID if omitted) |
1898
+ | `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
1899
+ | `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
1900
+ | `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](#single-step-job-shorthand). Mutually exclusive with `steps`. |
1901
+ | `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](#job-dependencies-needs) |
1902
+ | `options.rules` | `Rule[]` | no | Conditions for conditional execution |
1903
+ | `options.description` | `string` | no | Human-readable description |
1904
+ | `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
1905
+ | `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
1906
+ | `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
1907
+ | `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
1908
+ | `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. |
1909
+ | `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
1910
+ | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
1911
+ | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](../concurrency.md). |
1912
+ | `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
1913
+ | `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
1914
+ | `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
1915
+ | `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
1916
+ | `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
1917
+ | `options.afterStep` | `HookInput` | no | Hook that runs after each step |
1918
+ | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](../hooks.md#hook-timeout). |
1919
+ | `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](#timeouts). |
1920
+ | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](#per-job-resources) below. |
1921
+ | `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](#per-job-init) below. |
1918
1922
 
1919
1923
  **Returns:** `Job` -- an immutable job definition.
1920
1924
 
@@ -1935,20 +1939,27 @@ const build = job({
1935
1939
 
1936
1940
  #### runsOn forms
1937
1941
 
1938
- The `runsOn` parameter accepts three forms for targeting agents:
1942
+ A job's `runsOn` selects which agents may run it. Every label listed must be present on the agent (a subset match). It accepts three forms:
1939
1943
 
1940
1944
  ```typescript
1941
1945
  // 1. Simple string -- agent must have this label
1942
- runsOn: 'linux'
1946
+ runsOn: 'kici:os:linux'
1943
1947
 
1944
1948
  // 2. Array of required labels -- agent must have ALL labels
1945
- runsOn: ['linux', 'docker']
1949
+ runsOn: ['kici:os:linux', 'gpu']
1946
1950
 
1947
1951
  // 3. Object form with exclusions -- agent must have ALL required labels
1948
1952
  // and NONE of the excluded labels
1949
- runsOn: { labels: ['linux', 'docker'], exclude: ['gpu'] }
1953
+ runsOn: { labels: ['kici:os:linux'], exclude: ['kici:host:box-01'] }
1950
1954
  ```
1951
1955
 
1956
+ **The label model:**
1957
+
1958
+ - Every agent automatically reports `kici:os:<platform>`, `kici:arch:<cpu>`, and `kici:host:<hostname>`, so `runsOn: 'kici:os:linux'` targets any connected Linux agent without configuring labels — a fresh `kici init` matches out of the box.
1959
+ - Use **custom labels** (e.g. `'gpu'`, `'prod-pool'`) — defined in your scaler's `labelSet` — to target a specific agent pool.
1960
+ - You can also target scaler-assigned labels (`kici:agent:<backend>`, `kici:scaler:<name>`), but those names are deployment-specific, so custom labels are more portable.
1961
+ - `runsOn` is a _requirement_ on candidate agents, never a _grant_: targeting a label only narrows the candidate set. Users cannot _set_ `kici:` labels on agents — that namespace is reserved for the scaler and the agent's self-reported platform facts — but they may freely _target_ any label in `runsOn`.
1962
+
1952
1963
  **Semantics:**
1953
1964
 
1954
1965
  - **Required labels:** The agent must have every label in the `labels` array (or the string/array form).
@@ -2108,7 +2119,37 @@ The agent reads both files after the command succeeds, applies the delta, and th
2108
2119
 
2109
2120
  **Arrays run in order.** Passing `GenericInitConfig[]` runs the inits sequentially; each one's `$KICI_ENV` / `$KICI_PATH` delta is applied before the next runs, so a later init sees an earlier init's tools on `PATH`. The first init to fail stops the sequence and fails the job.
2110
2121
 
2111
- **`init: false`** is an explicit opt-out (reserved for a future auto-detect layer); it behaves the same as omitting `init`.
2122
+ **`init: false`** is an explicit opt-out; it behaves the same as omitting `init`.
2123
+
2124
+ #### Toolchain presets
2125
+
2126
+ For the common case, a typed preset removes the hand-written `run` block entirely. The agent expands the preset to the same generic init it would otherwise run.
2127
+
2128
+ - **`init: 'mise'`** -- zero-config. Installs mise, trusts and runs `mise install` against the committed mise config (`mise.toml` / `.mise.toml` / `.tool-versions`), hands mise's env + shims dir to subsequent steps, and caches mise's data dir under a key derived from the committed config (so a config change rotates the cache). The committed config is trusted automatically — committing it to your repo is the trust signal.
2129
+ - **`init: { mise: { cache, timeout, env, shell } }`** -- the same preset with overrides. These tune the generic fields a hand-written init exposes (minus `run`): `cache: false` disables caching, a `CacheSpec` replaces the default key/paths, and `timeout` / `env` / `shell` map straight through. `init: 'mise'` is exactly `init: { mise: {} }`.
2130
+
2131
+ ```typescript
2132
+ const build = job('build', {
2133
+ runsOn: 'linux',
2134
+ init: 'mise', // committed mise.toml pins the toolchain; jq, node, etc. land on PATH
2135
+ steps: [
2136
+ step('show-jq-version', async (ctx) => {
2137
+ const { stdout } = await ctx.$`jq --version`;
2138
+ ctx.log.info(`jq version: ${stdout.trim()}`);
2139
+ }),
2140
+ ],
2141
+ });
2142
+ ```
2143
+
2144
+ #### Auto-detect (`init: 'auto'`)
2145
+
2146
+ **`init: 'auto'`** detects the toolchain from committed files instead of naming a preset. The agent scans the clone root and selects a preset when a marker is present: `mise.toml` / `.mise.toml` / `.tool-versions` -> the mise preset. With no markers found, `'auto'` is a logged no-op.
2147
+
2148
+ `'auto'` is opt-in: an **unset** `init` does nothing even when the repo carries a `mise.toml` for local development. Use `'auto'` to enable detection and `false` to keep the explicit opt-out.
2149
+
2150
+ #### Cross-platform
2151
+
2152
+ The mise preset works on Linux, macOS, and Windows. On Linux and macOS mise is installed via its standalone install script; on Windows it is installed from its standalone GitHub release. The resulting toolchain reaches every step the same way on all three. On Windows the standalone mise binary requires the Microsoft Visual C++ runtime (`vc_redist.x64`) to be present on the agent host — install it once when provisioning a Windows agent that uses the mise preset.
2112
2153
 
2113
2154
  ## Step & job authoring patterns
2114
2155
 
@@ -3049,6 +3090,8 @@ Every variant carries the shared `EventBase` fields — `type`, `action`, `targe
3049
3090
 
3050
3091
  Matrix configurations expand a single job into multiple instances, one per parameter combination. Maximum 256 combinations.
3051
3092
 
3093
+ Expansion happens at **dispatch time**: the orchestrator materializes the matrix into N execution jobs — one per combination, each dispatched to its own agent — before any job runs. Each instance receives its combination as `ctx.matrix`. This is identical whether the workflow runs via `kici run local` or remotely through a webhook trigger, and the dashboard groups the N instances under one parent node.
3094
+
3052
3095
  ### Static array (single dimension)
3053
3096
 
3054
3097
  ```typescript
@@ -3103,6 +3146,8 @@ The function receives a `DynamicMatrixContext`:
3103
3146
 
3104
3147
  Must return `string[]` (single dimension) or `Record<string, string[]>` (multi-dimensional).
3105
3148
 
3149
+ A dynamic matrix is resolved at runtime, then materialized into N instances exactly like a static matrix. Because the combinations are not known until the function runs, the 256-combination cap (and the "zero combinations" guard) is enforced at that point: a dynamic matrix that resolves to more than 256 combinations, or to none, fails the job with a matrix-expansion error rather than dispatching.
3150
+
3106
3151
  ### Include and exclude
3107
3152
 
3108
3153
  Fine-tune matrix combinations on multi-dimensional matrices:
@@ -3141,6 +3186,35 @@ interface MatrixValues {
3141
3186
  }
3142
3187
  ```
3143
3188
 
3189
+ ### Consuming matrix outputs downstream
3190
+
3191
+ A downstream job that lists a matrix job in its `needs` receives a **keyed envelope** instead of a flat outputs object, because the upstream produced N sets of outputs (one per combination). `ctx.jobOutputs(matrixJob)` returns a `MatrixJobOutputs`:
3192
+
3193
+ ```typescript
3194
+ interface MatrixJobOutputs<T = Record<string, unknown>> {
3195
+ /** Keyed by the combination suffix — the text inside `(...)` of the child name. */
3196
+ byMatrix: Record<string, T>;
3197
+ /** Last-write-wins flat merge across children, in child (name) order. */
3198
+ merged: T;
3199
+ }
3200
+ ```
3201
+
3202
+ The suffix key matches the child job's display name: `byMatrix['a']` for a single-dimension `['a', 'b']` matrix, `byMatrix['linux, arm64']` for a multi-dimension combination. Use `isMatrixJobOutputs` (or `'byMatrix' in result`) to discriminate:
3203
+
3204
+ ```typescript
3205
+ import { isMatrixJobOutputs } from '@kici-dev/sdk';
3206
+
3207
+ step('collect', async ({ jobOutputs }) => {
3208
+ const out = jobOutputs(buildMatrixJob);
3209
+ if (isMatrixJobOutputs(out)) {
3210
+ console.log(out.byMatrix['a']); // outputs of the `a` combination
3211
+ console.log(out.merged); // last-write-wins across all combinations
3212
+ }
3213
+ });
3214
+ ```
3215
+
3216
+ The downstream job waits for **all** matrix combinations to terminate before it dispatches. A non-matrix upstream keeps the flat outputs shape. The envelope is identical under `kici run local` and the remote path.
3217
+
3144
3218
  ### Matrix type guards
3145
3219
 
3146
3220
  ```typescript
@@ -3556,6 +3630,68 @@ const deploy = job('deploy', {
3556
3630
  - Maximum 20 secret outputs per job
3557
3631
  - Maximum 64 KB per value
3558
3632
 
3633
+ ### ctx.kici.oidc.token({ audience })
3634
+
3635
+ Request a short-lived OIDC ID token for the current job, bound to an `audience`. The token is a signed JWT whose identity claims (`repository`, `ref`, `sha`, `kici_run_id`, `kici_job_id`) are derived by the build platform from the run context — a step cannot spoof them. Use it to authenticate the build to an external service that trusts the platform's OIDC issuer (for example, when generating build provenance).
3636
+
3637
+ ```typescript
3638
+ const publish = job('publish', {
3639
+ steps: [
3640
+ step('mint', async (ctx) => {
3641
+ const { token, expiresIn } = await ctx.kici.oidc.token({ audience: 'sigstore' });
3642
+ ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
3643
+ // Hand `token` to a tool that exchanges it with the trusting service.
3644
+ }),
3645
+ ],
3646
+ });
3647
+ ```
3648
+
3649
+ **Behavior:**
3650
+
3651
+ - The token is short-lived (about 10 minutes) and scoped to the current run and job.
3652
+ - The returned token value is automatically masked in step logs.
3653
+ - The step never holds platform credentials — the request is relayed through the orchestrator, which mints the token on the step's behalf.
3654
+ - Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.
3655
+
3656
+ ### ctx.attestProvenance({ subject })
3657
+
3658
+ Build, sign, and persist a build-provenance attestation for an artifact your step produced. KiCI assembles an in-toto SLSA v1.0 provenance statement whose build identity (`repository`, `ref`, `sha`, run/job ids) comes from the platform — not from the step — so it cannot be spoofed, signs it, and stores a verifiable bundle that the dashboard surfaces and the `kici verify-attestation` CLI checks.
3659
+
3660
+ The artifact is **caller-supplied**: give it either a precomputed digest or a path (relative to the step working directory) that KiCI digests with SHA-256. For a container image, pass the manifest digest your build tool emitted.
3661
+
3662
+ ```typescript
3663
+ const publish = job('publish', {
3664
+ steps: [
3665
+ step('build', async (ctx) => {
3666
+ await ctx.$`npm pack`;
3667
+ }),
3668
+ step('attest', async (ctx) => {
3669
+ // Digest a file KiCI hashes for you:
3670
+ const result = await ctx.attestProvenance({
3671
+ subject: { name: 'my-pkg-1.2.3.tgz', path: 'my-pkg-1.2.3.tgz' },
3672
+ });
3673
+ ctx.log.info(`Attestation stored at ${result.storageKey}`);
3674
+
3675
+ // Or supply a precomputed digest (e.g. a container manifest digest):
3676
+ await ctx.attestProvenance({
3677
+ subject: { name: 'ghcr.io/acme/app', digest: { sha256: '<manifest-digest>' } },
3678
+ });
3679
+ }),
3680
+ ],
3681
+ });
3682
+ ```
3683
+
3684
+ **Behavior:**
3685
+
3686
+ - The attestation is a signed [DSSE](https://github.com/secure-systems-lab/dsse) envelope over an [in-toto](https://in-toto.io) statement carrying the [SLSA v1.0](https://slsa.dev/spec/v1.0/provenance) provenance predicate.
3687
+ - It is signed with an ephemeral key bound to a platform-minted identity token, so it is **offline-verifiable** against the platform's published signing keys — no online lookup needed at verify time.
3688
+ - The bundle is persisted to object storage and recorded so the dashboard can show it and `kici verify-attestation` can retrieve it.
3689
+ - The returned `{ storageKey, subjectDigest, bundleMediaType }` identifies the stored bundle.
3690
+ - Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.
3691
+
3692
+ See the [build provenance guide](../provenance.md) for the end-to-end attest →
3693
+ verify → view journey, including how to verify a bundle with `kici verify-attestation`.
3694
+
3559
3695
  ## Secrets
3560
3696
 
3561
3697
  Workflows access secrets through `ctx.secrets` on `StepContext`. Use `await ctx.secrets.get('KEY')` to retrieve a value (rejects with `SecretNotFoundError` if the key is missing, fail-fast on typos), `ctx.secrets.has('KEY')` for a synchronous existence check, and `await ctx.secrets.expose('KEY')` when you need the value as a `process.env` entry for a child process.
@@ -4603,7 +4739,7 @@ This:
4603
4739
 
4604
4740
  1. Revokes the PAT on the server (preventing further use)
4605
4741
  2. Clears auth fields from the local config file
4606
- 3. Preserves non-auth settings (endpoint, routing key, connection mode)
4742
+ 3. Preserves non-auth settings (active org, default clusters, Platform endpoint)
4607
4743
 
4608
4744
  ## Organization management
4609
4745
 
@@ -4623,6 +4759,10 @@ kici org use <name-or-id>
4623
4759
 
4624
4760
  Name matching is case-insensitive. You can also use the organization ID directly.
4625
4761
 
4762
+ The active organization is both the scope for org-scoped commands (`kici runs list`, `kici diagnostics`, `kici secrets list`, …) **and** the default target for `kici run remote`. After `kici login` and `kici org use <org>`, `kici run remote` dispatches to that org through the Platform — that is the complete path to a remote run. Override the target for a single run with `kici run remote --org <id>`.
4763
+
4764
+ If an organization has more than one connected orchestrator cluster, set its default cluster once with `kici orchestrators use <name>` (list them with `kici orchestrators list`). `kici run remote` then targets that cluster unless you pass `--orchestrator <name>`. With a single connected orchestrator the cluster is selected automatically.
4765
+
4626
4766
  ### Show current organization
4627
4767
 
4628
4768
  ```bash
@@ -4633,18 +4773,15 @@ Displays the currently active organization name and ID.
4633
4773
 
4634
4774
  ## Auth status
4635
4775
 
4636
- The `kici status <run-id>` command displays run details including your authentication state:
4776
+ `kici org current` shows your current login state and active organization:
4637
4777
 
4638
4778
  ```bash
4639
- kici status <run-id>
4779
+ kici org current
4640
4780
  ```
4641
4781
 
4642
- The auth-related output includes:
4643
-
4644
- - Login state (logged in / not logged in)
4645
- - Active organization name
4646
- - PAT expiry date and time remaining
4647
- - Warning if PAT expires within 7 days
4782
+ It reports whether you are logged in and which organization is active. PAT
4783
+ expiry and the full list of your tokens are managed from the dashboard (see
4784
+ "Dashboard management" below).
4648
4785
 
4649
4786
  ## Personal access tokens
4650
4787
 
@@ -4767,7 +4904,8 @@ The CLI stores authentication data in `~/.kici/config` with `0600` permissions (
4767
4904
  - PAT token
4768
4905
  - PAT expiry date
4769
4906
  - Active organization ID
4770
- - Server endpoint URL
4907
+ - Per-org default orchestrator clusters
4908
+ - Platform endpoint URL
4771
4909
 
4772
4910
  ## Troubleshooting
4773
4911
 
@@ -5026,7 +5164,9 @@ kici run local push --keep-going
5026
5164
 
5027
5165
  Execute fixtures remotely through the full CI pipeline. Fixtures are defined in `.kici/tests/*.ts` using the `fixture()` factory function. Without arguments, lists available fixtures.
5028
5166
 
5029
- Requires `kici login` (an authenticated session) and a target orchestrator that has **cache storage configured** (`KICI_STORAGE_TYPE` = `s3` or `filesystem`) the command uploads your working-tree overlay to that storage for the agent to fetch. The quickstart orchestrators do not enable storage by default; see the [testing guide](testing-guide.md) for setup (including non-public / self-hosted S3 endpoints).
5167
+ Remote runs route through the Platform. Authenticate with a personal access token (`kici login`), then target an organization with `kici org use <org>` or the `--org` flag. The Platform relays the run to the org's orchestrator, while your working-tree overlay uploads directly to object storage see [How the run is routed](#how-the-run-is-routed) and [The two planes](#the-two-planes) below.
5168
+
5169
+ The orchestrator must have **cache storage configured** (`KICI_STORAGE_TYPE` = `s3` or `filesystem`) with a dev-reachable upload endpoint so the CLI's direct upload succeeds; see the [testing guide](testing-guide.md) and [Storage layout](../operator/orchestrator/storage-layout.md) for setup.
5030
5170
 
5031
5171
  ```bash
5032
5172
  kici run remote [fixture] [options]
@@ -5040,21 +5180,22 @@ kici run remote [fixture] [options]
5040
5180
 
5041
5181
  **Options:**
5042
5182
 
5043
- | Option | Default | Description |
5044
- | --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
5045
- | `--all` | `false` | Run all fixtures |
5046
- | `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
5047
- | `--parallel` | `false` | Run multiple fixtures concurrently |
5048
- | `--no-wait` | - | Fire and forget (print runIds, don't stream) |
5049
- | `--quiet` | `false` | Minimal output (only final result) |
5050
- | `--json` | `false` | Machine-readable JSON output |
5051
- | `--junit <path>` | none | JUnit XML output to file for CI integration |
5052
- | `--history` | `false` | Show table of recent test runs |
5053
- | `--routing-key <key>` | none | Override routing key for this run |
5054
- | `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted to the orchestrator (repeatable) |
5055
- | `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted to the orchestrator (repeatable) — see [testing guide](testing-guide.md) |
5056
- | `--debug` | `false` | Verbose internals |
5057
- | `--kici-dir <path>` | `.kici` | Path to .kici directory |
5183
+ | Option | Default | Description |
5184
+ | --------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
5185
+ | `--org <id>` | active | Target organization for this run (overrides `kici org use`) |
5186
+ | `--orchestrator <name>` | default | Target orchestrator cluster within the org (overrides the per-org default) |
5187
+ | `--all` | `false` | Run all fixtures |
5188
+ | `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
5189
+ | `--parallel` | `false` | Run multiple fixtures concurrently |
5190
+ | `--no-wait` | - | Fire and forget (print runIds, don't stream) |
5191
+ | `--quiet` | `false` | Minimal output (only final result) |
5192
+ | `--json` | `false` | Machine-readable JSON output |
5193
+ | `--junit <path>` | none | JUnit XML output to file for CI integration |
5194
+ | `--history` | `false` | Show table of recent test runs |
5195
+ | `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted (repeatable) |
5196
+ | `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted (repeatable) — see [testing guide](testing-guide.md) |
5197
+ | `--debug` | `false` | Verbose internals |
5198
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
5058
5199
 
5059
5200
  **Examples:**
5060
5201
 
@@ -5062,9 +5203,15 @@ kici run remote [fixture] [options]
5062
5203
  # List available fixtures
5063
5204
  kici run remote
5064
5205
 
5065
- # Run a single fixture
5206
+ # Run a single fixture against the active org
5066
5207
  kici run remote push-main
5067
5208
 
5209
+ # Target a specific org for this run
5210
+ kici run remote push-main --org xyz789ghi012
5211
+
5212
+ # Target a specific orchestrator cluster within the org
5213
+ kici run remote push-main --orchestrator us-east
5214
+
5068
5215
  # Run all push-related fixtures
5069
5216
  kici run remote push-*
5070
5217
 
@@ -5094,19 +5241,75 @@ kici run remote --history
5094
5241
  | 0 | All matched workflows passed |
5095
5242
  | 1 | One or more workflows failed |
5096
5243
 
5244
+ #### How the run is routed
5245
+
5246
+ A remote run is dispatched to your **active organization** — the one set with `kici org use <org>`, or overridden per-run with `--org <id>`. The org is resolved in this order:
5247
+
5248
+ 1. The `--org <id>` flag, if provided.
5249
+ 2. Otherwise the active org saved in your global config by `kici org use <org>`.
5250
+ 3. If neither is set, the command errors and asks you to select an org with `kici org use` or pass `--org`.
5251
+
5252
+ The orchestrator anchors the org without any manual webhook source: it auto-provisions a system-managed **remote source** (routing key `remote:<orgId>`) that maps to its bound organization, so even a zero-source org is immediately routable for remote runs. You never set a routing key for a remote run — selecting the org is enough.
5253
+
5254
+ When an org has more than one connected orchestrator cluster, the CLI picks the target cluster in this order:
5255
+
5256
+ 1. The `--orchestrator <name>` flag, if provided.
5257
+ 2. Otherwise the per-org default cluster, set with `kici orchestrators use <name>`.
5258
+ 3. If the org has exactly **one** connected orchestrator, it is auto-selected.
5259
+ 4. Otherwise the run errors with the list of connected clusters, and you pass `--orchestrator <name>` to choose one. Run `kici orchestrators list` to see the available cluster names.
5260
+
5261
+ #### The two planes
5262
+
5263
+ `kici run remote` uses two independent paths:
5264
+
5265
+ - **Control plane** — run initiation, trigger, status, log retrieval, and cancellation flow from your machine through the Platform, which relays them over a WebSocket connection to the org's orchestrator. Logs are delivered by the CLI polling the Platform for log chunks (tracked by a monotonic line cursor) and run status until the run reaches a terminal state; there is no direct streaming socket to the orchestrator.
5266
+ - **Data plane** — your working-tree overlay tarball uploads **directly** from your machine to the orchestrator's object store via a presigned PUT URL. The overlay never passes through the Platform. This is why the orchestrator's object-store upload endpoint must be reachable from your machine; see [Storage layout](../operator/orchestrator/storage-layout.md).
5267
+
5268
+ An orchestrator with no Platform connection cannot serve remote runs — the Platform is the service that offers them. For executing workflow steps on your own machine without an orchestrator (no scaler, agents, or environments), use [`kici run local`](#kici-run-local).
5269
+
5097
5270
  #### Fresh repos (no GitHub remote)
5098
5271
 
5099
5272
  `kici run remote` works even if the repo has never been pushed to GitHub. When no remote is detected:
5100
5273
 
5101
5274
  - The entire repo content is uploaded (not just a diff overlay)
5102
- - A synthetic routing key `local:<repo-name>` is used
5103
5275
  - The lock file is sent inline (no GitHub API fetch)
5104
5276
  - Steps that use git commands will fail (no `.git` directory in the remote workspace)
5105
5277
  - Build cache (`__build__` jobs) is skipped for local repos
5106
5278
  - Environments must have `allowLocalExecution: true` to be accessible from local runs (default is `false`)
5107
5279
 
5280
+ Destination routing is unchanged for fresh repos: the run still goes to your active org through the Platform.
5281
+
5108
5282
  For a detailed guide on writing fixtures, configuring secrets, and understanding the upload flow, see [Testing guide](testing-guide.md).
5109
5283
 
5284
+ #### kici orchestrators
5285
+
5286
+ List the orchestrator clusters connected to an organization, and set the per-org default cluster used by `kici run remote`. Requires `kici login` and an active org (or pass `--org`).
5287
+
5288
+ ```bash
5289
+ kici orchestrators list [--org <id>]
5290
+ kici orchestrators use <clusterName> [--org <id>]
5291
+ ```
5292
+
5293
+ **`kici orchestrators list`** prints the org's connected orchestrator clusters, so you know what to pass to `--orchestrator` (or to `kici orchestrators use`).
5294
+
5295
+ **`kici orchestrators use <clusterName>`** sets the default orchestrator cluster for the org, stored per-org in your global config. Subsequent `kici run remote` invocations target that cluster unless overridden with `--orchestrator`.
5296
+
5297
+ **Examples:**
5298
+
5299
+ ```bash
5300
+ # List the active org's connected clusters
5301
+ kici orchestrators list
5302
+
5303
+ # List a specific org's clusters
5304
+ kici orchestrators list --org xyz789ghi012
5305
+
5306
+ # Set the default cluster for the active org
5307
+ kici orchestrators use us-east
5308
+
5309
+ # Set the default cluster for a specific org
5310
+ kici orchestrators use us-east --org xyz789ghi012
5311
+ ```
5312
+
5110
5313
  ### kici test
5111
5314
 
5112
5315
  Preview which workflows match a trigger event (dry-run, no execution). Useful for verifying trigger configurations during development.
@@ -5181,9 +5384,7 @@ kici login [options]
5181
5384
  | --------------------------- | ------- | ---------------------------------------------- |
5182
5385
  | `--token <key>` | none | API key for direct authentication (legacy) |
5183
5386
  | `--device` | false | Force device authorization flow (headless/SSH) |
5184
- | `--endpoint <url>` | none | Orchestrator URL for direct connection |
5185
- | `--platform-endpoint <url>` | none | Platform relay URL |
5186
- | `--routing-key <key>` | none | Routing key for webhook source identification |
5387
+ | `--platform-endpoint <url>` | none | Platform API base URL |
5187
5388
 
5188
5389
  **Environment variables:**
5189
5390
 
@@ -5208,11 +5409,8 @@ kici login --device
5208
5409
  # Legacy API key login
5209
5410
  kici login --token kici_sk_abc123...
5210
5411
 
5211
- # Direct connection to orchestrator with API key
5212
- kici login --token kici_sk_abc123... --endpoint https://my-orchestrator.example.com
5213
-
5214
- # Platform relay connection with routing key
5215
- kici login --token kici_sk_abc123... --platform-endpoint https://platform.kici.dev --routing-key github:42
5412
+ # Log in against a self-hosted Platform
5413
+ kici login --platform-endpoint https://platform.example.com
5216
5414
 
5217
5415
  # Suppress browser opening (print authorize URL to stdout)
5218
5416
  KICI_BROWSER_CMD=none kici login
@@ -5230,7 +5428,7 @@ KICI_CALLBACK_PORT=19876 KICI_CONFIG_DIR=/tmp/kici-test kici login
5230
5428
 
5231
5429
  Revoke your personal access token on the server and clear local credentials.
5232
5430
 
5233
- If the server is unreachable, local credentials are still cleared (the PAT will expire automatically). Non-auth config fields (endpoint, routing key, etc.) are preserved.
5431
+ If the server is unreachable, local credentials are still cleared (the PAT will expire automatically). Non-auth config fields (active org, default clusters, etc.) are preserved.
5234
5432
 
5235
5433
  ```bash
5236
5434
  kici logout
@@ -5296,87 +5494,169 @@ Show the current active organization.
5296
5494
  kici org current
5297
5495
  ```
5298
5496
 
5299
- ### kici status
5497
+ ### kici diagnostics
5300
5498
 
5301
- Show details for a specific test run. Fetches from the orchestrator with fallback to local history.
5499
+ Show the orchestrators, scalers, and agents serving your organization the
5500
+ terminal equivalent of the dashboard Diagnostics page. Reads the same
5501
+ org-scoped data the dashboard does, so it needs `kici login` and an active org
5502
+ (`kici org use <name>`).
5302
5503
 
5303
- The status output includes an **auth section** showing login state, active organization, and PAT expiry. A warning appears when the PAT expires within 7 days.
5504
+ The output has three parts: a one-line header (runs in the last 24h, success
5505
+ rate, average duration, queued/running job counts), any infrastructure alerts
5506
+ (only shown when present), and a tree of each orchestrator with its scalers and
5507
+ agents. Each agent line shows its labels, platform/architecture, active/maximum
5508
+ concurrency, and heartbeat age.
5304
5509
 
5305
- For a failed run, the output prints a `Reason:` line with the run's failure reason and shows the failed job's error inline, so you can see why a run failed without opening the dashboard. When provisioning an agent failed before any step ran, this reason is the captured scaler error (for example a missing binary or an unpullable image) rather than a generic "no agents available" message.
5510
+ ```bash
5511
+ kici diagnostics [options]
5512
+ ```
5513
+
5514
+ **Options:**
5515
+
5516
+ | Option | Default | Description |
5517
+ | --------------------- | ------- | --------------------------------------------------- |
5518
+ | `--json` | `false` | Machine-readable JSON output |
5519
+ | `--verbose` | `false` | Show extended per-agent fields (host, node, memory) |
5520
+ | `--orchestrator <id>` | all | Scope the tree to one orchestrator connection id |
5521
+
5522
+ **Examples:**
5306
5523
 
5307
5524
  ```bash
5308
- kici status <run-id> [options]
5525
+ # Show the full infrastructure tree
5526
+ kici diagnostics
5527
+
5528
+ # Extended per-agent detail
5529
+ kici diagnostics --verbose
5530
+
5531
+ # Only one orchestrator's scalers and agents
5532
+ kici diagnostics --orchestrator conn-abc123
5533
+
5534
+ # Machine-readable output
5535
+ kici diagnostics --json
5309
5536
  ```
5310
5537
 
5311
- **Arguments:**
5538
+ ### kici runs
5539
+
5540
+ Inspect and manage execution runs from the terminal — the equivalent of the
5541
+ dashboard Runs page. All `kici runs` subcommands read/write the same org-scoped
5542
+ data as the dashboard, so they require `kici login` and an active org
5543
+ (`kici org use <name>`).
5312
5544
 
5313
- | Argument | Required | Description |
5314
- | -------- | -------- | -------------- |
5315
- | `run-id` | yes | Run identifier |
5545
+ #### kici runs list
5546
+
5547
+ List runs with optional filters. Output is a table (run id, workflow, status,
5548
+ branch, trigger, started, duration); pagination is reported at the bottom.
5549
+
5550
+ ```bash
5551
+ kici runs list [options]
5552
+ ```
5316
5553
 
5317
5554
  **Options:**
5318
5555
 
5319
- | Option | Default | Description |
5320
- | -------------- | ------- | ------------------------------------------------------------ |
5321
- | `--logs` | `false` | Stream logs (live for active runs, historical for completed) |
5322
- | `--job <name>` | all | Filter logs to a specific job |
5323
- | `--json` | `false` | Machine-readable JSON output |
5556
+ | Option | Default | Description |
5557
+ | ----------------------- | ------- | --------------------------------------------- |
5558
+ | `--status <s>` | all | Filter by run status |
5559
+ | `--workflow <w>` | all | Filter by workflow name |
5560
+ | `--branch <b>` | all | Filter by branch/ref |
5561
+ | `--repo <r>` | all | Filter by repository |
5562
+ | `--trigger <t>` | all | Filter by trigger type |
5563
+ | `--source <routingKey>` | all | Filter by source routing key |
5564
+ | `--since <ts>` | none | Only runs since this ISO-8601 or epoch ms |
5565
+ | `--page <n>` | `1` | Page number (server page size is fixed at 20) |
5566
+ | `--json` | `false` | Machine-readable JSON output |
5324
5567
 
5325
- **Examples:**
5568
+ ```bash
5569
+ kici runs list
5570
+ kici runs list --status running
5571
+ kici runs list --workflow ci --branch main
5572
+ kici runs list --json | jq '.runs[].runId'
5573
+ ```
5574
+
5575
+ #### kici runs show
5576
+
5577
+ Show a run's summary header plus its jobs-and-steps tree (name, status,
5578
+ duration, exit code). If the run id is not on the Platform but exists in your
5579
+ local run history (from `kici run local`), the local record is shown instead.
5326
5580
 
5327
5581
  ```bash
5328
- # Show run summary
5329
- kici status abc123
5582
+ kici runs show <run-id> [options]
5583
+ ```
5330
5584
 
5331
- # Show full logs (historical for completed runs, live streaming for active runs)
5332
- kici status abc123 --logs
5585
+ | Option | Default | Description |
5586
+ | -------- | ------- | ---------------------------- |
5587
+ | `--json` | `false` | Machine-readable JSON output |
5333
5588
 
5334
- # Show logs for a specific job
5335
- kici status abc123 --logs --job build
5589
+ ```bash
5590
+ kici runs show abc123
5591
+ kici runs show abc123 --json
5592
+ ```
5336
5593
 
5337
- # Machine-readable output
5338
- kici status abc123 --json
5594
+ #### kici runs logs
5595
+
5596
+ Print each job/step's log lines in order, with headers.
5597
+
5598
+ ```bash
5599
+ kici runs logs <run-id> [options]
5339
5600
  ```
5340
5601
 
5341
- When `--json` is set, `kici` emits only the JSON document on stdout — the
5342
- `kici v<version>` banner is suppressed so the output is safe to pipe into
5343
- `jq` or `JSON.parse`. The same holds for the other `--json` commands (`kici run
5344
- remote --json`, `kici workflows list --json`) and for `--quiet`.
5602
+ | Option | Default | Description |
5603
+ | -------------- | ------- | -------------------------------------- |
5604
+ | `--job <name>` | all | Only print logs for this job |
5605
+ | `-f, --follow` | `false` | Tail logs for a live run until it ends |
5606
+ | `--json` | `false` | Machine-readable JSON output |
5345
5607
 
5346
- ### kici cancel
5608
+ ```bash
5609
+ kici runs logs abc123
5610
+ kici runs logs abc123 --job build
5611
+ kici runs logs abc123 --follow
5612
+ ```
5613
+
5614
+ #### kici runs rerun
5347
5615
 
5348
- Cancel a running workflow or all runs on a branch.
5616
+ Re-trigger a completed run. Prints the new run id. The server enforces a short
5617
+ cooldown between reruns of the same run.
5349
5618
 
5350
5619
  ```bash
5351
- kici cancel [run-id] [options]
5620
+ kici runs rerun <run-id> [options]
5352
5621
  ```
5353
5622
 
5354
- **Arguments:**
5623
+ | Option | Default | Description |
5624
+ | -------- | ------- | ---------------------------- |
5625
+ | `--json` | `false` | Machine-readable JSON output |
5626
+
5627
+ ```bash
5628
+ kici runs rerun abc123
5629
+ ```
5630
+
5631
+ #### kici runs cancel
5632
+
5633
+ Cancel a single run, or all in-progress runs on a branch.
5634
+
5635
+ ```bash
5636
+ kici runs cancel [run-id] [options]
5637
+ ```
5355
5638
 
5356
5639
  | Argument | Required | Description |
5357
5640
  | -------- | -------- | ---------------- |
5358
5641
  | `run-id` | no | Run ID to cancel |
5359
5642
 
5360
- **Options:**
5361
-
5362
5643
  | Option | Default | Description |
5363
5644
  | ----------------- | ------- | ------------------------------------------- |
5364
5645
  | `--force` | `false` | Force cancel (kill immediately, skip hooks) |
5365
5646
  | `--branch <name>` | none | Cancel all in-progress runs on this branch |
5366
5647
 
5367
- **Examples:**
5368
-
5369
5648
  ```bash
5370
- # Cancel a specific run
5371
- kici cancel abc123
5372
-
5373
- # Force cancel (kill immediately)
5374
- kici cancel abc123 --force
5375
-
5376
- # Cancel all runs on a branch
5377
- kici cancel --branch feature/wip
5649
+ kici runs cancel abc123
5650
+ kici runs cancel abc123 --force
5651
+ kici runs cancel --branch feature/wip
5378
5652
  ```
5379
5653
 
5654
+ When `--json` is set on any of these commands, `kici` emits only the JSON
5655
+ document on stdout — the `kici v<version>` banner is suppressed — so the output
5656
+ is safe to pipe into `jq` or `JSON.parse`. The same holds for the other
5657
+ `--json` commands (`kici run remote --json`, `kici workflows list --json`) and
5658
+ for `--quiet`.
5659
+
5380
5660
  ### kici approve
5381
5661
 
5382
5662
  Approve a held [approval gate](approvals.md) so the run resumes. Identify the held element by run ID, optionally narrowed to a job and step.
@@ -5779,6 +6059,70 @@ kici admin drain-worker --url http://worker-2.internal:10143
5779
6059
  | 0 | Drain request accepted |
5780
6060
  | 1 | Error (unreachable or request fail) |
5781
6061
 
6062
+ ### kici verify-attestation
6063
+
6064
+ Verify a KiCI build-provenance attestation bundle offline. A bundle is the signed package a workflow step produces via `ctx.attestProvenance(...)`: a DSSE-wrapped SLSA in-toto statement, the ephemeral public key that signed it, and the KiCI identity token that anchors the build context. For the end-to-end attest → verify → view journey, see the [build provenance guide](./provenance.md). Verification establishes the full chain — the identity token verifies against the trusted issuer's JWKS, the DSSE signature verifies against the bundled key, and the statement's build context must match the token's claims (a mismatch is a hard failure). When an `[artifact]` is given, its SHA-256 digest is also matched against the attestation subject.
6065
+
6066
+ ```bash
6067
+ kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-file> [options]
6068
+ ```
6069
+
6070
+ **Arguments:**
6071
+
6072
+ | Argument | Required | Description |
6073
+ | ------------ | -------- | --------------------------------------------------------------------- |
6074
+ | `[artifact]` | no | Artifact path to digest-check against the attestation subject digest. |
6075
+
6076
+ **Options:**
6077
+
6078
+ | Option | Required | Description |
6079
+ | ---------------------------- | -------- | ----------------------------------------------------------------------------------------- |
6080
+ | `--bundle <path-or-url>` | yes | Path or `http(s)` URL to the attestation bundle JSON. |
6081
+ | `--trust-root <url-or-file>` | yes | Trusted issuer (see below). The token issuer is pinned to it, never taken from the token. |
6082
+ | `--audience <aud>` | no | Expected token audience (defaults to the KiCI provenance audience). |
6083
+ | `--json` | no | Print the structured verification result as JSON instead of human-readable output. |
6084
+
6085
+ **Trust root:** the verifier never trusts the issuer named inside the token — you supply the trusted issuer out-of-band via `--trust-root`, in one of two forms:
6086
+
6087
+ - **Online — an HTTPS issuer URL.** The verifier fetches `<url>/.well-known/openid-configuration`, reads its `issuer` and `jwks_uri`, and fetches the JWKS. The token's `iss` is pinned to the discovery document's `issuer`.
6088
+ - **Offline — a self-contained trust-root file.** A local JSON file with the issuer and JWKS inlined, so no network access is needed (air-gapped verification):
6089
+
6090
+ ```json
6091
+ {
6092
+ "issuer": "https://platform.example/issuer",
6093
+ "jwks": {
6094
+ "keys": [
6095
+ { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "alg": "ES256", "kid": "..." }
6096
+ ]
6097
+ }
6098
+ }
6099
+ ```
6100
+
6101
+ **Examples:**
6102
+
6103
+ ```bash
6104
+ # Online: verify a bundle against a deployed issuer, digest-checking the artifact
6105
+ kici verify-attestation ./dist/app.tgz \
6106
+ --bundle ./app.tgz.kici.json \
6107
+ --trust-root https://platform.example/issuer
6108
+
6109
+ # Offline / air-gapped: verify against a self-contained trust-root file
6110
+ kici verify-attestation ./dist/app.tgz \
6111
+ --bundle ./app.tgz.kici.json \
6112
+ --trust-root ./kici-trust-root.json
6113
+
6114
+ # Machine-readable result for scripting
6115
+ kici verify-attestation --bundle ./app.tgz.kici.json \
6116
+ --trust-root https://platform.example/issuer --json
6117
+ ```
6118
+
6119
+ **Exit codes:**
6120
+
6121
+ | Code | Meaning |
6122
+ | ---- | ----------------------------------------------------------------------------------- |
6123
+ | 0 | Verified — signature, identity, build context (and digest, if checked) all pass |
6124
+ | 1 | Not verified, or an error (missing flag, unreadable bundle, unreachable trust root) |
6125
+
5782
6126
  ## Workflow discovery
5783
6127
 
5784
6128
  The CLI discovers workflows by scanning `.kici/workflows/*.ts` (or `.mjs` in MJS mode). Each file should `export default` a single workflow:
@@ -6110,7 +6454,7 @@ The lock file (`kici.lock.json`) is a JSON file with the following top-level fie
6110
6454
 
6111
6455
  | Field | Description |
6112
6456
  | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6113
- | `schemaVersion` | Lock file schema version (currently 15). Incremented on breaking format changes. |
6457
+ | `schemaVersion` | Lock file schema version (currently 17). Incremented on breaking format changes. |
6114
6458
  | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
6115
6459
  | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
6116
6460
  | `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
@@ -6428,7 +6772,7 @@ kici run remote push-main --junit results.xml
6428
6772
  kici run remote push-main --no-wait
6429
6773
 
6430
6774
  # Check status later
6431
- kici status <run-id>
6775
+ kici runs show <run-id>
6432
6776
  ```
6433
6777
 
6434
6778
  ### Cancellation
@@ -6596,17 +6940,17 @@ kici run remote --history
6596
6940
  ### Run details
6597
6941
 
6598
6942
  ```bash
6599
- # Show run summary (tries orchestrator first, falls back to local history)
6600
- kici status <run-id>
6943
+ # Show run summary (reads the Platform, falls back to local history)
6944
+ kici runs show <run-id>
6601
6945
 
6602
6946
  # Show full logs
6603
- kici status <run-id> --logs
6947
+ kici runs logs <run-id>
6604
6948
 
6605
6949
  # Show logs for a specific job
6606
- kici status <run-id> --logs --job build
6950
+ kici runs logs <run-id> --job build
6607
6951
 
6608
6952
  # Machine-readable output
6609
- kici status <run-id> --json
6953
+ kici runs show <run-id> --json
6610
6954
  ```
6611
6955
 
6612
6956
  ## Scaffolding with kici init
@@ -6723,6 +7067,178 @@ To actually change how you sign in, use your account console as described above.
6723
7067
 
6724
7068
  ---
6725
7069
 
7070
+ ## Approval gates
7071
+
7072
+ Source: https://docs.kici.dev/user/approvals/
7073
+
7074
+ An **approval gate** pauses execution until an authorized person approves it. Execution resumes from exactly where it paused; a rejection (or an expired hold) fails the run.
7075
+
7076
+ You declare a gate in your workflow with `requireApproval`. It is available at three levels of granularity:
7077
+
7078
+ - **Step** — pause mid-job, before a specific step runs. The agent holds the live workspace (with all prior-step state intact) for the duration of the wait.
7079
+ - **Job** — hold the job before any of its steps run.
7080
+ - **Workflow** — hold the whole run before any job is dispatched.
7081
+
7082
+ Approvers are named as **teams** and **users**. A team is an operator-defined group of org members; your workflow code may name a team but can never change its membership, which is what makes a team clause a real gate rather than a suggestion. See [Approval gates (operator guide)](../operator/approvals.md) for how operators define teams, the approval queue, and expiry; see [the architecture overview](../architecture/approvals.md) for how a hold is evaluated and resumed.
7083
+
7084
+ ## Quick start
7085
+
7086
+ Hold a deploy job until a member of the `leads` team approves:
7087
+
7088
+ ```typescript
7089
+ import { workflow, job, step, push } from '@kici-dev/sdk';
7090
+
7091
+ export default workflow('deploy', {
7092
+ on: [push({ branches: ['main'] })],
7093
+ jobs: [
7094
+ job('deploy-production', {
7095
+ runsOn: 'default',
7096
+ requireApproval: [{ team: 'leads' }],
7097
+ steps: [step('deploy', async (ctx) => ctx.$`deploy --prod`)],
7098
+ }),
7099
+ ],
7100
+ });
7101
+ ```
7102
+
7103
+ When the run reaches this job, it is held instead of dispatched. The held run appears in the dashboard approval queue and can be released from there or with the [`kici approve`](#approving-from-the-cli) command. Once a member of `leads` approves, the job dispatches normally.
7104
+
7105
+ ## The `requireApproval` field
7106
+
7107
+ `requireApproval` accepts three forms.
7108
+
7109
+ ### Shorthand: `true`
7110
+
7111
+ ```typescript
7112
+ job('deploy', {
7113
+ runsOn: 'default',
7114
+ requireApproval: true,
7115
+ steps: [
7116
+ /* ... */
7117
+ ],
7118
+ });
7119
+ ```
7120
+
7121
+ `requireApproval: true` holds the element until **any** org member who can act on approvals signs off — anyone with the `environments:write` or `ci_trust:write` permission. Use it when you want a manual gate without restricting who may release it.
7122
+
7123
+ ### Approver list (AND)
7124
+
7125
+ ```typescript
7126
+ requireApproval: [{ team: 'leads' }, { user: 'cto' }],
7127
+ ```
7128
+
7129
+ A list of approver clauses is an **AND** list: every clause must be satisfied before the element is released.
7130
+
7131
+ - `{ team: 'leads' }` is satisfied once **any** member of the `leads` team approves.
7132
+ - `{ user: 'cto' }` is satisfied once the user `cto` approves.
7133
+
7134
+ A single approver may satisfy more than one clause. If `cto` is also a member of `leads`, one approval from `cto` satisfies both `{ team: 'leads' }` and `{ user: 'cto' }`, releasing the element. A user is named by their KiCI user identifier (their linked identity), and a team by its name as defined by your operator.
7135
+
7136
+ There is no OR or nested logic — clauses are always a flat AND list.
7137
+
7138
+ ### Object form: reason and timeout
7139
+
7140
+ ```typescript
7141
+ requireApproval: {
7142
+ approvers: [{ team: 'security' }, { team: 'leads' }],
7143
+ reason: 'Production deploy requires security + leads sign-off',
7144
+ timeout: 7200, // seconds
7145
+ },
7146
+ ```
7147
+
7148
+ | Field | Type | Description |
7149
+ | ----------- | ------------------ | --------------------------------------------------------------------------------------------------- |
7150
+ | `approvers` | `ApproverClause[]` | The AND list of `{ team }` / `{ user }` clauses. An empty list means "any approval-capable member". |
7151
+ | `reason` | `string` | A human-readable label shown in the dashboard queue and the held-for-approval status check. |
7152
+ | `timeout` | `number` | Per-gate expiry in **seconds**, overriding the org default. On expiry the element is rejected. |
7153
+
7154
+ When `timeout` is omitted, the gate uses the org's default approval expiry (set by the operator). On expiry, the held element is rejected and the run fails — see [expiry](../operator/approvals.md#expiry).
7155
+
7156
+ ## Granularity
7157
+
7158
+ The same `requireApproval` field is accepted on a workflow, a job, and a step.
7159
+
7160
+ ### Workflow-level
7161
+
7162
+ A workflow-level gate holds the entire run before any job is dispatched:
7163
+
7164
+ ```typescript
7165
+ export default workflow('release', {
7166
+ on: [push({ branches: ['main'] })],
7167
+ requireApproval: [{ team: 'release-managers' }],
7168
+ jobs: [buildJob, publishJob],
7169
+ });
7170
+ ```
7171
+
7172
+ ### Job-level
7173
+
7174
+ A job-level gate holds just that job; other jobs in the run proceed normally:
7175
+
7176
+ ```typescript
7177
+ job('publish', {
7178
+ runsOn: 'default',
7179
+ requireApproval: [{ team: 'leads' }],
7180
+ steps: [
7181
+ /* ... */
7182
+ ],
7183
+ });
7184
+ ```
7185
+
7186
+ ### Step-level
7187
+
7188
+ A step-level gate pauses mid-job, immediately before the named step. Earlier steps in the job have already run and their workspace state is preserved across the wait:
7189
+
7190
+ ```typescript
7191
+ job('migrate-and-deploy', {
7192
+ runsOn: 'default',
7193
+ steps: [
7194
+ step('build-plan', async (ctx) => ctx.$`./gen-migration-plan.sh`),
7195
+ step('apply-migration', {
7196
+ requireApproval: [{ team: 'dba' }],
7197
+ run: async (ctx) => ctx.$`./apply-migration.sh`,
7198
+ }),
7199
+ step('deploy', async (ctx) => ctx.$`deploy --prod`),
7200
+ ],
7201
+ });
7202
+ ```
7203
+
7204
+ Here `build-plan` runs, then the job pauses for a `dba` approval. On approval, `apply-migration` runs against the exact workspace `build-plan` produced, followed by `deploy`. A rejection or expiry fails the job.
7205
+
7206
+ Because a step-level hold keeps an agent and its workspace occupied for the whole human wait, prefer job- or workflow-level gates when you do not need prior-step state, and keep step-level timeouts short. See the [operator note on agent occupancy](../operator/approvals.md#agent-occupancy-during-step-level-holds).
7207
+
7208
+ ## Mandatory vs. explicit gates
7209
+
7210
+ `requireApproval` is the **explicit** gate — a deliberate "pause for a human here" written by the workflow author. It composes with the **mandatory** gate an operator can attach to a protected environment via required reviewers (see [Environments](environments.md#required-reviewers)). When both apply to the same job, all clauses from both sources must be satisfied before the job is released. The two funnel into one held-element mechanism, so the dashboard queue and `kici approve` work the same way regardless of which source held the element.
7211
+
7212
+ ## Approving from the CLI
7213
+
7214
+ Approve or reject a held element with the `kici` CLI:
7215
+
7216
+ ```bash
7217
+ # Approve a workflow-level hold
7218
+ kici approve <run-id>
7219
+
7220
+ # Approve a held job
7221
+ kici approve <run-id> --job deploy-production
7222
+
7223
+ # Approve a held step
7224
+ kici approve <run-id> --job migrate-and-deploy --step apply-migration
7225
+
7226
+ # Reject (a reason is required)
7227
+ kici reject <run-id> --job deploy-production --reason "Wrong release branch"
7228
+ ```
7229
+
7230
+ You must be eligible for at least one unsatisfied clause — being a member of a named team or being a named user. The orchestrator verifies eligibility against the operator-defined teams, so naming a team in your workflow can never let an ineligible person release the gate. The command reports whether the element was released, how many clauses remain, or that it was rejected. See [`kici approve`](cli-reference.md#kici-approve) for the full command reference.
7231
+
7232
+ You can also approve from the dashboard approval queue. See [Dashboard](dashboard.md#approval-queue).
7233
+
7234
+ ## See also
7235
+
7236
+ - [Environments](environments.md) — operator-required reviewers on protected environments.
7237
+ - [Approval gates (operator guide)](../operator/approvals.md) — teams, the approval queue, expiry, and self-approval.
7238
+ - [Approval gates (architecture)](../architecture/approvals.md) — the unified hold model and the step-level round-trip.
7239
+
7240
+ ---
7241
+
6726
7242
  ## Concurrency groups
6727
7243
 
6728
7244
  Source: https://docs.kici.dev/user/concurrency/
@@ -7199,7 +7715,7 @@ The page uses a responsive multi-panel layout that adapts to screen width:
7199
7715
 
7200
7716
  - **Wide desktop (>= 1200px)** -- three-panel layout with a resizable job tree (left), content area (center), and metadata sidebar (right). Two draggable dividers between the panels let you resize them. Panel sizes persist to `localStorage`.
7201
7717
  - **Medium desktop (< 1200px)** -- two-panel layout with the job tree and content area. Metadata is accessible via a "Show metadata" drawer button.
7202
- - **Mobile (< 768px)** -- stacked layout with the job tree at the top and content below. Metadata is available as a tab alongside Logs, Payload, Timeline, and Summary.
7718
+ - **Mobile (< 768px)** -- stacked layout with the job tree at the top and content below. Metadata is available as a tab alongside Logs, Payload, Timeline, Graph, and Summary.
7203
7719
 
7204
7720
  ### Run header
7205
7721
 
@@ -7265,7 +7781,9 @@ The content area has the following tabs:
7265
7781
  - **Logs** (default) -- shows log output for the selected job or step
7266
7782
  - **Payload** -- webhook payload viewer showing the raw event payload that triggered the run. This tab appears only for runs triggered by a webhook event (and re-runs of those, which copy the original payload); runs started by a schedule, manual schedule, lifecycle event, or another run carry no payload, so the tab is hidden for them
7267
7783
  - **Timeline** -- CSS Gantt chart showing the execution timeline of all jobs, with percentage-based bars and striped animation for running jobs. A **Provisioning** milestones section between the dispatch and execution phases plots scaler lifecycle events for the run — including a **Provisioning failed** marker when the scaler could not bring an agent up
7784
+ - **Graph** -- dependency graph (DAG) view of the run's jobs: each job is a node, and arrows point from a job to the jobs that depend on it. Matrix jobs appear as one node per variant. Each node shows the job name, status, and duration; a job's left accent border and the status line are colored by run state (running nodes pulse, failed nodes are red, skipped nodes are dimmed). Click a node to open that job's details (the same selection the Timeline and right panel use); hover a node to highlight what it depends on and what depends on it. Dependency edges flagged to run even when the upstream failed are drawn as dashed orange arrows. The Timeline tab remains the place to see durations and overlap on a time axis
7268
7785
  - **Summary** -- contextual overview scoped to the current selection (run-level trigger/repo/timing info, or job-level execution context with environment variables, runtime info, and sandbox details)
7786
+ - **Attestations** -- build-provenance attestations produced by the run's steps (via `ctx.attestProvenance`), one row per attested artifact with a **verified** badge and a bundle download. See [Build provenance and attestations](./provenance.md#viewing-attestations-in-the-dashboard) for what the badge checks and how to verify a bundle against a specific file.
7269
7787
 
7270
7788
  On wide desktop (>= 1200px), Metadata is shown in a dedicated sidebar panel instead of as a tab.
7271
7789
 
@@ -9184,29 +9702,37 @@ registries: [
9184
9702
 
9185
9703
  ## Security model
9186
9704
 
9187
- - **Per-environment scoping.** Every `tokenSecret` and `installEnv` entry is qualified with an environment name. The orchestrator runs the same protection-rule pipeline (branch / trust / concurrency / reviewer / wait-timer) against each named environment **before** resolving any secret, so a workflow that wants a `production` token from a feature branch is rejected exactly like a job that tries to deploy to `production` from a feature branch.
9705
+ - **Per-environment scoping.** Every `tokenSecret` and `installEnv` entry is qualified with an environment name. The orchestrator runs the same protection-rule pipeline (branch / trust / concurrency / reviewer / wait-timer) against each named environment **before** resolving any secret, so a workflow that wants a `production` token from a feature branch is rejected exactly like a job that tries to deploy to `production` from a feature branch. A reviewer-gated install environment **pauses** the whole workflow dispatch as a workflow-scoped held run instead of resolving the token — see [Reviewer-gated installs](#reviewer-gated-installs) below.
9188
9706
  - **Untrusted contributors get no tokens.** When a fork PR is dispatched and the contributor-trust resolution returns anything other than `trusted`, the orchestrator strips both `npmRegistries` and `installEnvSecrets` out of the dispatch. The install runs without auth and fails naturally on the first private dep — fork PRs cannot ever observe a registry token, even if a misconfigured environment lacks an explicit `requiredTrustTier`.
9189
9707
  - **Lifecycle scripts disabled.** Whenever a private registry is in scope, the agent runs the install with `--ignore-scripts` (npm or pnpm alike). A malicious `preinstall` / `postinstall` hook in committed `package.json` cannot read the synthesized token env vars, even though they exist in the install subprocess. For a pnpm workspace, the agent builds your in-repo dependency closure as a separate step **after** the install's auth is torn down, so build scripts never see the tokens either.
9190
9708
  - **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
9191
9709
  - **Job-scoped env-var names.** The synthesized auth env var is `KICI_NPM_TOKEN_<jobIdShort>_<i>` where `jobIdShort` is the first 8 chars of the dispatched job id. The name is unguessable from outside the install subprocess and not reused across jobs.
9192
9710
  - **`.npmrc` restored.** Whatever the agent appended for one install is stripped (or the file unlinked) on cleanup, so the workspace is never permanently modified.
9193
9711
 
9712
+ ## Reviewer-gated installs
9713
+
9714
+ When the named install environment carries a protection rule that holds — a required reviewer (`hold`) or a wait timer (`wait`) — the install gate **pauses the whole workflow dispatch** instead of rejecting it. The run is created in the `held` state, no jobs are queued, and a workflow-scoped row appears on the held-runs page with a `Workflow` scope badge.
9715
+
9716
+ - **Reviewer hold:** the run waits for an approver. On approval the dispatch resumes from the install gate, resolves the token, and dispatches its jobs as a normal run. On rejection the run transitions to `cancelled` — no jobs ever run.
9717
+ - **Wait timer:** the run waits out the timer and resumes automatically when it elapses.
9718
+
9719
+ A `reject` protection outcome (for example a disabled environment or a branch the environment forbids) still fails the dispatch loudly with a clear reason, exactly as before — the orchestrator never dispatches a run with an unresolved install token.
9720
+
9194
9721
  ## Limitations
9195
9722
 
9196
9723
  - **`registries:` is workflow-level only in v1.** Per-job overrides aren't supported — there is one shared `.kici/` per workspace, so a per-job `registries:` would be physically nonsensical.
9197
- - **A non-pass protection-rule outcome rejects the whole workflow dispatch.** Today, if the named environment requires reviewer approval (`hold` action) or hits concurrency (`queue` / `wait`) for the install gate, the entire workflow dispatch is rejected with a clear reason. Workflow-scoped held-runs (which would let the install wait for an approver instead of failing outright) are tracked as a follow-up — until then, choose an environment whose protection rules `pass` for the branches that need to install private deps.
9198
9724
  - **Container registries (Docker Hub, ECR, GHCR) are out of scope.** This feature covers **npm** registry auth only. Container image pulls travel through the executor backend's own credential paths.
9199
9725
 
9200
9726
  ## Observability
9201
9727
 
9202
9728
  The orchestrator exposes Prometheus counters and a histogram under the `kici_orch_install_secrets_*` prefix on its `/metrics` endpoint. They populate the **Install secrets resolution** Grafana dashboard and let operators graph install-secrets activity without digging through Loki.
9203
9729
 
9204
- | Metric | Type | Labels | What it tells you |
9205
- | ------------------------------------------------------------- | --------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9206
- | `kici_orch_install_secrets_decisions_total` | Counter | `decision`, `reason` | Pass / reject volume. Reject reasons enumerate the failure mode: `malformed_ref`, `invalid_url_scheme`, `env_not_found`, `protection_rule_block`, `missing_token`, `missing_install_env`, etc. |
9207
- | `kici_orch_install_secrets_npm_registry_used_total` | Counter | `channel`, `provider`, `scope` | Per-channel + per-scope usage. `channel=registries` is Option A, `channel=install_env` is Option C. `scope=default` marks a no-scope default registry; `scope=-` marks Option C entries. |
9208
- | `kici_orch_install_secrets_contributor_stripped_total` | Counter | `trust_tier` | Number of dispatches where registry tokens were stripped because the contributor tier wasn't `trusted` (fork PRs from unknown / known contributors). Expected to be 0 in single-tenant orgs. |
9209
- | `kici_orch_install_secrets_token_resolution_duration_seconds` | Histogram | `environment` | Latency of per-environment secret resolution. Pathological tails (>500ms) usually mean a Vault timeout or a slow Postgres replica. |
9730
+ | Metric | Type | Labels | What it tells you |
9731
+ | ------------------------------------------------------------- | --------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9732
+ | `kici_orch_install_secrets_decisions_total` | Counter | `decision`, `reason` | Pass / reject / hold volume. `decision=hold` (reason `held`) counts dispatches paused at a reviewer-gated install environment. Reject reasons enumerate the failure mode: `malformed_ref`, `invalid_url_scheme`, `env_not_found`, `protection_rule_block`, `missing_token`, `missing_install_env`, etc. |
9733
+ | `kici_orch_install_secrets_npm_registry_used_total` | Counter | `channel`, `provider`, `scope` | Per-channel + per-scope usage. `channel=registries` is Option A, `channel=install_env` is Option C. `scope=default` marks a no-scope default registry; `scope=-` marks Option C entries. |
9734
+ | `kici_orch_install_secrets_contributor_stripped_total` | Counter | `trust_tier` | Number of dispatches where registry tokens were stripped because the contributor tier wasn't `trusted` (fork PRs from unknown / known contributors). Expected to be 0 in single-tenant orgs. |
9735
+ | `kici_orch_install_secrets_token_resolution_duration_seconds` | Histogram | `environment` | Latency of per-environment secret resolution. Pathological tails (>500ms) usually mean a Vault timeout or a slow Postgres replica. |
9210
9736
 
9211
9737
  The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-secrets.json`; if you maintain your own monitoring stack, you can import it directly.
9212
9738
 
@@ -9218,6 +9744,197 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
9218
9744
 
9219
9745
  ---
9220
9746
 
9747
+ ## Build provenance and attestations
9748
+
9749
+ Source: https://docs.kici.dev/user/provenance/
9750
+
9751
+ Build provenance is a signed, verifiable statement of **what produced an
9752
+ artifact** — the source repository, commit, ref, workflow path, and builder that
9753
+ ran. When a workflow step attests an artifact, KiCI records that statement,
9754
+ signs it, and makes it retrievable so anyone can later prove the artifact came
9755
+ from a specific KiCI run and was not swapped along the way.
9756
+
9757
+ This is the same idea behind supply-chain attestation systems like
9758
+ [SLSA](https://slsa.dev/spec/v1.0/provenance): a downstream consumer (a release
9759
+ gate, a security audit, a `"show me the provenance"` request) can verify the
9760
+ artifact's origin without trusting the person who handed it over.
9761
+
9762
+ ## What an attestation contains
9763
+
9764
+ An attestation is a self-contained bundle holding three things:
9765
+
9766
+ - An **in-toto SLSA v1.0 statement** describing the build: the subject artifact
9767
+ (name + content digest) and the provenance predicate (source repository,
9768
+ commit, ref, workflow, run/job identifiers, timestamps).
9769
+ - A **[DSSE](https://github.com/secure-systems-lab/dsse) signature** over that
9770
+ statement, made with an ephemeral signing key generated for the run.
9771
+ - A short-lived **OIDC identity token** issued by the KiCI platform that binds
9772
+ the signature to the build identity. The token's identity claims
9773
+ (`repository`, `ref`, `sha`, run/job ids) are derived by the platform from the
9774
+ run itself — a step cannot forge them.
9775
+
9776
+ Because the bundle carries the identity token and the public signing key, it is
9777
+ **offline-verifiable**: a verifier checks it against the platform's published
9778
+ signing keys with no per-attestation online lookup.
9779
+
9780
+ ## Attesting an artifact in a workflow
9781
+
9782
+ Call `ctx.attestProvenance({ subject })` from a step after you have produced the
9783
+ artifact:
9784
+
9785
+ ```typescript
9786
+ import { workflow, job, step } from '@kici-dev/sdk';
9787
+
9788
+ export default workflow('release', {
9789
+ on: { push: { branches: ['main'] } },
9790
+ jobs: [
9791
+ job('publish', {
9792
+ steps: [
9793
+ step('build', async (ctx) => {
9794
+ await ctx.$`npm pack`;
9795
+ }),
9796
+ step('attest', async (ctx) => {
9797
+ const result = await ctx.attestProvenance({
9798
+ subject: { name: 'my-pkg-1.2.3.tgz', path: 'my-pkg-1.2.3.tgz' },
9799
+ });
9800
+ ctx.log.info(`Attestation stored at ${result.storageKey}`);
9801
+ }),
9802
+ ],
9803
+ }),
9804
+ ],
9805
+ });
9806
+ ```
9807
+
9808
+ The **subject is caller-supplied** — you name the artifact and give KiCI either a
9809
+ path or a precomputed digest:
9810
+
9811
+ - `{ name, path }` — a path relative to the step working directory. KiCI reads
9812
+ the file and computes its SHA-256 digest.
9813
+ - `{ name, digest }` — a precomputed digest. For a container image, pass the OCI
9814
+ manifest digest your build tool emitted:
9815
+
9816
+ ```typescript
9817
+ await ctx.attestProvenance({
9818
+ subject: { name: 'ghcr.io/acme/app', digest: { sha256: '<manifest-digest>' } },
9819
+ });
9820
+ ```
9821
+
9822
+ The identity token is fetched and masked in logs automatically — you never
9823
+ handle it. The call returns `{ storageKey, subjectDigest, bundleMediaType }`
9824
+ identifying the stored bundle.
9825
+
9826
+ `ctx.attestProvenance` is only available inside a running job step. Calling it
9827
+ during local execution rejects with a clear error.
9828
+
9829
+ ### Requesting a raw identity token
9830
+
9831
+ `ctx.attestProvenance` builds on a lower-level primitive you can call directly
9832
+ when you need the identity token for a different tool:
9833
+
9834
+ ```typescript
9835
+ step('mint', async (ctx) => {
9836
+ const { token, expiresIn } = await ctx.kici.oidc.token({ audience: 'sigstore' });
9837
+ ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
9838
+ // Hand `token` to a tool that exchanges it with a service trusting the issuer.
9839
+ });
9840
+ ```
9841
+
9842
+ The token is a short-lived (about 10 minutes) signed JWT scoped to the current
9843
+ run and job. Its identity claims (`repository`, `ref`, `sha`, `kici_run_id`,
9844
+ `kici_job_id`) are derived by the platform from the run context, so a step cannot
9845
+ spoof them. The returned token value is automatically masked in step logs, and
9846
+ the step never holds platform credentials — the request is relayed through the
9847
+ orchestrator, which mints the token on the step's behalf. Like
9848
+ `attestProvenance`, it is only available inside a running job step.
9849
+
9850
+ ## Verifying an attestation
9851
+
9852
+ Verify a bundle with the `kici verify-attestation` command. It establishes the
9853
+ full chain offline: the identity token verifies against the trusted issuer's
9854
+ JWKS, the DSSE signature verifies against the bundled signing key, and the
9855
+ statement's build context must match the token's identity claims (a mismatch is
9856
+ a hard failure).
9857
+
9858
+ ```bash
9859
+ kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-file>
9860
+ ```
9861
+
9862
+ You supply the trusted issuer out-of-band via `--trust-root` — the verifier
9863
+ never trusts the issuer named inside the token. There are two forms:
9864
+
9865
+ - **Online — an HTTPS issuer URL.** The verifier fetches
9866
+ `<url>/.well-known/openid-configuration`, reads its `issuer` and `jwks_uri`,
9867
+ and fetches the JWKS. The token's `iss` is pinned to the discovery document's
9868
+ `issuer`.
9869
+ - **Offline — a self-contained trust-root file.** A local JSON file with the
9870
+ issuer and JWKS inlined, for air-gapped verification:
9871
+
9872
+ ```json
9873
+ {
9874
+ "issuer": "https://platform.example/issuer",
9875
+ "jwks": {
9876
+ "keys": [
9877
+ { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "alg": "ES256", "kid": "..." }
9878
+ ]
9879
+ }
9880
+ }
9881
+ ```
9882
+
9883
+ Pass an optional `[artifact]` to also digest-check the file against the
9884
+ attestation subject — this is what binds the attestation to a specific set of
9885
+ bytes. Omit it to verify the signatures and identity only. Use `--json` for a
9886
+ machine-readable result. The command exits `0` when everything verifies and `1`
9887
+ when it does not (or on an error such as a missing flag or unreachable trust
9888
+ root).
9889
+
9890
+ ```bash
9891
+ # Verify a bundle against a deployed issuer, digest-checking the artifact:
9892
+ kici verify-attestation ./dist/app.tgz \
9893
+ --bundle ./app.tgz.kici.json \
9894
+ --trust-root https://platform.example/issuer
9895
+
9896
+ # Air-gapped: verify against a self-contained trust-root file:
9897
+ kici verify-attestation ./dist/app.tgz \
9898
+ --bundle ./app.tgz.kici.json \
9899
+ --trust-root ./kici-trust-root.json
9900
+ ```
9901
+
9902
+ The full flag reference is in the [CLI reference](./cli-reference.md#kici-verify-attestation).
9903
+
9904
+ ## Viewing attestations in the dashboard
9905
+
9906
+ <!-- help:run-attestations#viewing-attestations-in-the-dashboard -->
9907
+
9908
+ The run detail page has an **Attestations** tab listing each artifact a run's
9909
+ steps attested (via `ctx.attestProvenance`), one row per artifact.
9910
+
9911
+ Each row shows:
9912
+
9913
+ - **Status** — a **verified** badge computed in your browser. It checks the
9914
+ attestation's signature, the build identity, and the build context against the
9915
+ trusted provenance issuer. **verified** (green) means all of those pass;
9916
+ **failed** (red) shows why in a tooltip; **unverifiable** means the provenance
9917
+ issuer is not configured.
9918
+ - **Job / Artifact / Digest / Created** — the producing job, the artifact name,
9919
+ its content digest, and when it was recorded.
9920
+ - **Download** — saves the signed bundle as a `.sigstore.json` file.
9921
+
9922
+ The badge does **not** re-hash the artifact bytes — the dashboard does not have
9923
+ the artifact. To bind the attestation to a specific file, run
9924
+ `kici verify-attestation <artifact> --bundle <bundle>`. A run with no
9925
+ attestations shows an empty state.
9926
+
9927
+ <!-- /help:run-attestations -->
9928
+
9929
+ ## See also
9930
+
9931
+ - [SDK runtime reference](./sdk/runtime.md) — the `ctx.attestProvenance` and
9932
+ `ctx.kici.oidc.token` step APIs in full.
9933
+ - [CLI reference](./cli-reference.md#kici-verify-attestation) — every
9934
+ `kici verify-attestation` flag and exit code.
9935
+
9936
+ ---
9937
+
9221
9938
  ## Secrets
9222
9939
 
9223
9940
  Source: https://docs.kici.dev/user/secrets/
@@ -9438,6 +10155,45 @@ export default workflow('deploy', {
9438
10155
  });
9439
10156
  ```
9440
10157
 
10158
+ ### Injecting decrypted sops values into the environment
10159
+
10160
+ KiCI does **not** scan your repository for `*.enc.yaml` files and auto-decrypt them into the environment at job init — nothing in a job runs `sops` on your behalf, and resolved secrets are never auto-injected as environment variables (see [Security notes](./sdk/runtime.md#security-notes)). Decryption is always something your workflow does explicitly: provision the age (or other) decryption key as a KiCI secret, expose it for the step, run `sops -d`, and decide what to do with the output.
10161
+
10162
+ When you want the decrypted values available as environment variables — not just written to a file — decrypt early and export the values through `$KICI_ENV` (or `ctx.setEnv`). Anything appended to `$KICI_ENV` becomes an environment variable for every later step in the same job, so a single decrypt step can populate the environment for the whole job:
10163
+
10164
+ ```typescript
10165
+ import { workflow, job, step, push } from '@kici-dev/sdk';
10166
+
10167
+ export default workflow('deploy', {
10168
+ on: push({ branches: ['main'] }),
10169
+ jobs: [
10170
+ job('decrypt-and-deploy', {
10171
+ runsOn: 'default',
10172
+ environment: 'production',
10173
+ steps: [
10174
+ step('decrypt-to-env', async (ctx) => {
10175
+ await ctx.secrets.exposeFile('SOPS_AGE_KEY_FILE', {
10176
+ sources: ctx.secrets.list().filter((k) => k.startsWith('AGE_KEY_')),
10177
+ divider: '\n',
10178
+ });
10179
+ // Decrypt to dotenv format, then append every KEY=value line to
10180
+ // $KICI_ENV so subsequent steps see them as environment variables.
10181
+ await ctx.$`sops -d --output-type dotenv secrets.enc.yaml >> "$KICI_ENV"`;
10182
+ }),
10183
+ step('deploy', async (ctx) => {
10184
+ // Values decrypted above are now ordinary env vars here.
10185
+ await ctx.$`./deploy.sh`;
10186
+ }),
10187
+ ],
10188
+ }),
10189
+ ],
10190
+ });
10191
+ ```
10192
+
10193
+ Decrypted values exported this way follow the same rules as any other `$KICI_ENV` / `ctx.setEnv` export: last-write-wins on a repeated key, and a key that collides with an operator-injected secret is ignored (the operator value wins). See [Exporting env from shell commands](./sdk/runtime.md#exporting-env-from-shell-commands-kici_env--kici_path) for the full `$KICI_ENV` contract.
10194
+
10195
+ If you only need the decrypted material as a file on disk (the common `kubectl` / `gcloud` case), skip the env hop and redirect to a file instead — see the [canonical sops example](#canonical-sops-example) above.
10196
+
9441
10197
  ## API reference
9442
10198
 
9443
10199
  | Method | Signature | Description |
@@ -9800,6 +10556,123 @@ and confirm the App is installed on that repo.
9800
10556
 
9801
10557
  ---
9802
10558
 
10559
+ ## Local filesystem (file://) source
10560
+
10561
+ Source: https://docs.kici.dev/user/providers/local-file/
10562
+
10563
+ The **local** provider lets the orchestrator run workflows from a git
10564
+ repository that already lives on the agent's filesystem — cloned via a
10565
+ `file://` URL rather than fetched from a remote forge. There is no webhook
10566
+ from GitHub or GitLab; the operator drives runs explicitly with the
10567
+ `kici-admin` CLI or a generated `post-receive` hook.
10568
+
10569
+ > **This is an operator-curated source, not daily developer CI.** Use it for
10570
+ > globally-registered or policy workflows the operator controls — a vendored
10571
+ > repo baked into an agent image, a repo synced onto a host out-of-band, a
10572
+ > golden internal pipeline. For ordinary per-developer CI driven by pull
10573
+ > requests and pushes, use the [GitHub App provider](./github.md) or the
10574
+ > [universal-git provider](./universal-git.md) against a real forge.
10575
+
10576
+ ## Trust caveat (read first)
10577
+
10578
+ A local source uses signature verification `none` — there is no remote forge
10579
+ to sign the webhook payload, so the orchestrator cannot authenticate the
10580
+ trigger. **Only register repos you trust.** Anyone who can reach the
10581
+ orchestrator's webhook route for this source, or push to the repo on disk,
10582
+ can drive a run. Treat the repo path as a trusted operator input, the same
10583
+ way you treat the orchestrator's own configuration.
10584
+
10585
+ ## Register a source
10586
+
10587
+ ```bash
10588
+ kici-admin source add local \
10589
+ --org <orgId> \
10590
+ --path /abs/path/to/repo \
10591
+ --name my-local-repo
10592
+ ```
10593
+
10594
+ - `--path` must be an **absolute** directory on the agent filesystem. It is
10595
+ the base path the orchestrator's lock-file fetcher reads
10596
+ (`<path>/.kici/kici.lock.json`) and the base for the `file://` clone the
10597
+ agent performs.
10598
+ - `--clone-url-base <url>` is optional. By default the agent clones via
10599
+ `file://<path>`. Supply a `git://` or `http://` base when the agent does
10600
+ **not** share the orchestrator's filesystem and must fetch the repo over a
10601
+ git server instead (see "Per-scaler reachability" below).
10602
+
10603
+ Update the path or name later:
10604
+
10605
+ ```bash
10606
+ kici-admin source update-local <id> --path /new/abs/path
10607
+ kici-admin source update-local <id> --name new-name
10608
+ ```
10609
+
10610
+ Remove it:
10611
+
10612
+ ```bash
10613
+ kici-admin source remove <routingKey> --local
10614
+ ```
10615
+
10616
+ List and inspect (local sources render their `repoBasePath`):
10617
+
10618
+ ```bash
10619
+ kici-admin source list --org <orgId>
10620
+ kici-admin source get <id>
10621
+ ```
10622
+
10623
+ ## Trigger runs
10624
+
10625
+ A local repo has no forge to send webhooks, so you trigger runs yourself.
10626
+
10627
+ **One-shot, by hand:**
10628
+
10629
+ ```bash
10630
+ kici-admin source trigger-local <id>
10631
+ ```
10632
+
10633
+ The command reads the repo's current HEAD ref and commit SHA, builds a
10634
+ GitHub-shaped `push` payload, and POSTs it to the orchestrator's generic
10635
+ webhook route. Override the ref/sha/event explicitly when needed:
10636
+
10637
+ ```bash
10638
+ kici-admin source trigger-local <id> --event push --ref refs/heads/main --sha <sha>
10639
+ ```
10640
+
10641
+ **On every push, via a hook:**
10642
+
10643
+ ```bash
10644
+ kici-admin source install-hook <id>
10645
+ ```
10646
+
10647
+ This writes a `post-receive` hook into the repo so that every push to it
10648
+ triggers a run automatically — the local equivalent of a forge webhook.
10649
+
10650
+ ## Per-scaler reachability (operator's responsibility)
10651
+
10652
+ The orchestrator accepts a local source on **any** scaler backend and does
10653
+ **not** verify that the repo is actually reachable inside the agent. Making
10654
+ the path reachable is the operator's job. On a container or Firecracker
10655
+ scaler the orchestrator logs a reachability warning when it registers the
10656
+ source, but it does not reject it.
10657
+
10658
+ | Scaler | How the repo must be reachable in the agent |
10659
+ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10660
+ | bare-metal | The agent runs on the host, so the `--path` host directory is used directly. |
10661
+ | container | Bake the repo into the agent image, or bind-mount it at the **same absolute path** the source was registered with. Alternatively register `--clone-url-base` pointing at a git server the container can reach. |
10662
+ | Firecracker | The repo must be present on the microVM rootfs at the registered path, or reachable via a `--clone-url-base` git server. |
10663
+
10664
+ If the path is not reachable inside the agent, the clone fails at run time —
10665
+ the run is created and then fails, rather than being silently dropped.
10666
+
10667
+ ## See also
10668
+
10669
+ - [Universal-git provider](./universal-git.md) — for a remote forge (or any
10670
+ `http://` git server) when there is no shared filesystem.
10671
+ - [GitHub App provider](./github.md) — the flagship source for pull-request
10672
+ CI with Checks.
10673
+
10674
+ ---
10675
+
9803
10676
  ## Universal-git provider
9804
10677
 
9805
10678
  Source: https://docs.kici.dev/user/providers/universal-git/
@@ -9826,6 +10699,13 @@ No mirror, no GitHub App, no `checkout: false` escape hatch. The same
9826
10699
  trigger matching, global-workflow policy, and agent execution pipeline
9827
10700
  that back the GitHub App source also serve universal-git sources.
9828
10701
 
10702
+ > **No shared filesystem between orchestrator and agent?** Universal-git is
10703
+ > the right choice for the **remote-agent** case — point it at an `http://`
10704
+ > git server and the agent clones over the network. When the repo instead
10705
+ > lives on the agent's own filesystem (a vendored / operator-curated repo),
10706
+ > use a [local `file://` source](./local-file.md) and drive it with the
10707
+ > `kici-admin` CLI.
10708
+
9829
10709
  ## Which preset do I need?
9830
10710
 
9831
10711
  KiCI ships canonical presets so you don't have to spell out JSONPath for
@@ -10045,7 +10925,7 @@ with the right PEM.
10045
10925
 
10046
10926
  Source: https://docs.kici.dev/architecture/data-flows/
10047
10927
 
10048
- This document describes the key data flows through the KiCI architecture: webhook delivery, job execution, dependency caching, re-run and cancel, trace ID propagation, internal event routing, and generic webhook ingestion.
10928
+ This document describes the key data flows through the KiCI architecture: webhook delivery, job execution, developer-initiated remote runs, dependency caching, re-run and cancel, trace ID propagation, internal event routing, and generic webhook ingestion.
10049
10929
 
10050
10930
  > **Lock file schema version:** The lock file uses schema version 15, which adds per-job init config on top of v14's declarative cache specs, v11's `LockInlineValue` for pure function inline evaluation, v10's simplified negative patterns (! prefix in repos/paths arrays), v9's global workflow repos matching, and v8's runsOn polymorphic type support.
10051
10931
 
@@ -10117,6 +10997,41 @@ The agent delegates job execution to an `ExecutionSandbox` (container, bare-meta
10117
10997
  7. **Report** -- Send final `job.status` back to orchestrator with step results and timing
10118
10998
  8. **Cleanup** -- Tear down sandbox and remove work directory
10119
10999
 
11000
+ ## Remote run flow (`kici run remote`)
11001
+
11002
+ A developer running `kici run remote` from a working tree initiates a run through the three-tier relay without a provider webhook. The flow splits into two independent planes: a **control plane** through the Platform relay, and a **data plane** that uploads the working-tree overlay straight to object storage.
11003
+
11004
+ ```
11005
+ Developer machine Platform relay Orchestrator Object store
11006
+ | | | |
11007
+ |-- upload-init (control) ----->|--- WS relay ----------->| |
11008
+ | (org, cluster, overlay | |-- mint presigned |
11009
+ | metadata, inline lock) | | PUT URL ---------->|
11010
+ |<-- presigned PUT URL ---------|<--- WS relay -----------| |
11011
+ | | | |
11012
+ |== overlay tarball PUT (data plane) ===========================================>|
11013
+ | | | |
11014
+ |-- trigger (control) --------->|--- WS relay ----------->|-- dispatch jobs |
11015
+ | | | (agents fetch |
11016
+ | | | overlay) |
11017
+ |-- poll logs + status -------->|--- WS relay ----------->| |
11018
+ |<-- log chunks + status -------|<--- WS relay -----------| |
11019
+ ```
11020
+
11021
+ ### Control plane
11022
+
11023
+ Run initiation (`upload-init`), the trigger, status polling, log retrieval, and cancellation all flow from the developer machine to the Platform, which relays them over its WebSocket connection to the org's orchestrator. The developer machine never talks to the orchestrator's HTTP API directly. Logs are delivered by the CLI polling the Platform for log chunks — tracked by a monotonic line cursor — and run status until the run reaches a terminal state; there is no direct streaming socket between the developer machine and the orchestrator.
11024
+
11025
+ ### Data plane
11026
+
11027
+ The working-tree overlay tarball uploads **directly** from the developer machine to the orchestrator's object store via a pre-signed PUT URL minted during `upload-init`. The overlay bytes never pass through the Platform. Because of this split, only the object store needs to be reachable from the developer machine — the orchestrator can sit behind a private network. See [Storage layout](../operator/orchestrator/storage-layout.md) for the upload-endpoint configuration.
11028
+
11029
+ ### Org anchor
11030
+
11031
+ The run is dispatched to the developer's active organization (selected with `kici org use`, or overridden per-run). The orchestrator anchors its bound organization with a system-managed **remote source** (routing key `remote:<orgId>`) that it auto-provisions — no manual webhook source is required, so a zero-source org is immediately routable for remote runs. The Platform forces the run's routing key to `remote:<orgId>` server-side; the developer never sets a routing key. When an org has more than one connected orchestrator cluster, the CLI selects the target cluster explicitly (or relies on the per-org default), and a single connected cluster is auto-selected.
11032
+
11033
+ Remote runs are offered by the Platform; an orchestrator with no Platform connection cannot serve them. Executing workflow steps on the developer machine with no orchestrator is the separate `kici run local` path.
11034
+
10120
11035
  ## Source and dependency caching flow
10121
11036
 
10122
11037
  KiCI runs two orchestrator-side caches — the **source tarball cache** (raw `.kici/` directory minus `node_modules/`) and the **dependency tarball cache** (packed `node_modules/`). Both use a build-then-execute pattern: the orchestrator checks the caches before dispatching execution jobs, and if the source cache is cold a build agent populates both in one pass.
@@ -10947,7 +11862,7 @@ Shared business logic used by all three tiers. Single source of truth for cross-
10947
11862
  - Environment allowlist (safe env var filtering)
10948
11863
  - Secrets management (secret context resolution)
10949
11864
  - Environment model (scoped secrets, env merge, protection gates)
10950
- - Label utilities (platform label derivation, runsOn normalization, `kici:*` reserved namespace, role labels)
11865
+ - Label utilities (platform label derivation, runsOn normalization, `kici:*` set-only reserved namespace, role labels)
10951
11866
  - Audit policy and retention (per-action access-log sampling, warm-retention windows for cold-store eligibility, federated activity row schema)
10952
11867
  - Scaler backend type enum (`container`, `bare-metal`, `firecracker`, `kubernetes`)
10953
11868
  - Registration trigger type enum (registerable trigger discriminator)