@kici-dev/compiler 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/cli.js +34 -10
  2. package/dist/commands/compile.d.ts +6 -0
  3. package/dist/commands/compile.js +6 -3
  4. package/dist/commands/docs.d.ts +8 -8
  5. package/dist/commands/docs.js +35 -16
  6. package/dist/commands/index.d.ts +4 -2
  7. package/dist/commands/index.js +3 -2
  8. package/dist/commands/init.js +2 -2
  9. package/dist/commands/org.js +2 -2
  10. package/dist/commands/pat.d.ts +27 -0
  11. package/dist/commands/pat.js +76 -0
  12. package/dist/commands/preview.d.ts +88 -0
  13. package/dist/commands/{test.js → preview.js} +15 -14
  14. package/dist/commands/run.d.ts +27 -2
  15. package/dist/commands/run.js +117 -18
  16. package/dist/commands/test.d.ts +4 -0
  17. package/dist/commands/types.d.ts +2 -0
  18. package/dist/commands/types.js +1 -1
  19. package/dist/commands/verify-attestation.d.ts +4 -1
  20. package/dist/commands/verify-attestation.js +26 -10
  21. package/dist/fixtures/describe-event.d.ts +6 -0
  22. package/dist/fixtures/describe-event.js +18 -0
  23. package/dist/fixtures/picker.d.ts +19 -0
  24. package/dist/fixtures/picker.js +64 -0
  25. package/dist/generators/secrets-dts.js +2 -0
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +2 -2
  28. package/dist/llm-context/llms-architecture.txt +1440 -0
  29. package/dist/llm-context/llms-cli.txt +2509 -0
  30. package/dist/llm-context/llms-features.txt +2491 -0
  31. package/dist/llm-context/llms-full.txt +1364 -361
  32. package/dist/llm-context/llms-getting-started.txt +519 -0
  33. package/dist/llm-context/llms-patterns.txt +1324 -0
  34. package/dist/llm-context/llms-providers.txt +805 -0
  35. package/dist/llm-context/llms-sdk.txt +3844 -0
  36. package/dist/llm-context/llms.txt +16 -1
  37. package/dist/local-executor/index.js +42 -4
  38. package/dist/local-executor/job-runner.d.ts +2 -0
  39. package/dist/local-executor/job-runner.js +38 -6
  40. package/dist/local-executor/types.d.ts +2 -0
  41. package/dist/lockfile/generator.d.ts +10 -2
  42. package/dist/lockfile/generator.js +112 -49
  43. package/dist/remote/history.d.ts +1 -1
  44. package/dist/remote/history.js +1 -1
  45. package/dist/remote/local-repo-identity.d.ts +32 -0
  46. package/dist/remote/local-repo-identity.js +74 -0
  47. package/dist/remote/platform-client.d.ts +6 -0
  48. package/dist/remote/prod-defaults.d.ts +8 -0
  49. package/dist/remote/prod-defaults.js +9 -1
  50. package/dist/remote/uploader.js +1 -0
  51. package/dist/templates/agents-md.d.ts +1 -1
  52. package/dist/templates/agents-md.js +2 -2
  53. package/dist/templates/package-json.js +1 -1
  54. package/dist/test-runner/rule-evaluator.d.ts +1 -1
  55. package/dist/test-runner/rule-evaluator.js +2 -1
  56. package/dist/test-runner/step-context.d.ts +1 -1
  57. package/dist/test-runner/step-context.js +8 -2
  58. package/dist/types.d.ts +38 -7
  59. package/dist/types.js +5 -1
  60. package/package.json +4 -7
  61. package/sbom.spdx.json +35 -35
@@ -12,75 +12,75 @@ Documentation for workflow authors -- people writing CI/CD pipelines in TypeScri
12
12
 
13
13
  ## Pages
14
14
 
15
- ### [Getting started with KiCI](getting-started.md)
15
+ ### [Getting started with KiCI](https://docs.kici.dev/user/getting-started/)
16
16
 
17
17
  Install the SDK and compiler, write your first workflow, compile it to a lock file, and test it locally with simulated events. Covers prerequisites (Node.js 24+, pnpm), the `kici init` command for scaffolding, and the relationship between workflows, the lock file, and the three-tier runtime.
18
18
 
19
- ### [SDK reference](sdk-reference.md)
19
+ ### [SDK reference](https://docs.kici.dev/user/sdk-reference/)
20
20
 
21
21
  Complete API reference for `@kici-dev/sdk`. Covers all factory functions (`workflow()`, `job()`, `step()`), trigger builders (`pr()`, `push()`), rule functions (`rule()`, `skip()`), matrix configuration (static arrays, static objects, dynamic functions, include/exclude), and the `StepContext` interface that steps receive at runtime.
22
22
 
23
- ### [Lock file and workflow drift](lock-file-and-drift.md)
23
+ ### [Lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/)
24
24
 
25
25
  Why the lock file must stay in sync with workflow source, how to commit both together, using pre-commit and CI to catch drift early, and the agent-side hash verification when compiling from source.
26
26
 
27
- ### [CLI reference](cli-reference.md)
27
+ ### [CLI reference](https://docs.kici.dev/user/cli-reference/)
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 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.
29
+ All CLI commands provided by `@kici-dev/compiler`: `kici compile` (with watch mode and check mode), `kici run` (local and remote execution), `kici preview` (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
- ### [Workflow patterns](workflow-patterns.md)
31
+ ### [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/)
32
32
 
33
33
  Common patterns for building real-world CI/CD workflows. Includes examples for basic CI pipelines with job dependencies, monorepo path-based triggering, conditional jobs with rules, matrix builds across Node versions, dynamic jobs generated at runtime, Docker-based step execution, and parallel test splitting.
34
34
 
35
- ### [Dashboard](dashboard.md)
35
+ ### [Dashboard](https://docs.kici.dev/user/dashboard/)
36
36
 
37
37
  Guide to the KiCI web dashboard. Covers navigation (sidebar, org switcher, mobile bottom tabs), run list (table columns, filters, pagination, empty states), run detail (resizable two-panel layout, job tree, step selection, metadata tabs), log viewer (ANSI color rendering, search, permalink, copy), settings page (tabbed layout), theme toggle, keyboard shortcuts, and error pages.
38
38
 
39
- ### [Testing guide](testing-guide.md)
39
+ ### [Testing guide](https://docs.kici.dev/user/testing-guide/)
40
40
 
41
41
  How to run and write tests for KiCI workflows, including remote test execution with `kici run remote`, fixture-based testing, and overlay mode for uncommitted changes.
42
42
 
43
- ### [Environments](environments.md)
43
+ ### [Environments](https://docs.kici.dev/user/environments/)
44
44
 
45
45
  Configure deployment environments (staging, production, review/\*) with variables, scoped secrets, and protection rules. Covers the SDK API (`environment`, `env`, `concurrencyGroup` on jobs), the 8-layer variable merge precedence, protection rules (branch restrictions, required reviewers, wait timers, concurrency), dashboard management, type generation, and migration from the legacy contexts system.
46
46
 
47
- ### [Environment variables](env-vars.md)
47
+ ### [Environment variables](https://docs.kici.dev/user/env-vars/)
48
48
 
49
49
  Reference for all `KICI_*` environment variables supported by the CLI. Covers authentication overrides (OIDC issuer, client ID, project ID), browser behavior (custom browser command, fixed callback port), development mode, and usage examples for CI/CD, self-hosted, and headless environments.
50
50
 
51
- ### [CLI authentication](cli-auth.md)
51
+ ### [CLI authentication](https://docs.kici.dev/user/cli-auth/)
52
52
 
53
53
  Authenticate the KiCI CLI with browser-based OAuth (default), device authorization flow (for headless environments), or API key paste (for CI/CD pipelines). Covers org management and PATs.
54
54
 
55
- ### [Event system](events.md)
55
+ ### [Event system](https://docs.kici.dev/user/events/)
56
56
 
57
57
  Event model concepts: event types, the registration model, event matching, and circuit breaker protection. Understanding this distinction is key to working with non-git triggers like schedules, custom events, and generic webhooks.
58
58
 
59
- ### [Lifecycle hooks](hooks.md)
59
+ ### [Lifecycle hooks](https://docs.kici.dev/user/hooks/)
60
60
 
61
61
  SDK hook API for cancel, cleanup, success, failure, and step-level callbacks. Hooks run at specific points in the execution lifecycle to react to outcomes and perform cleanup.
62
62
 
63
- ### [Concurrency groups](concurrency.md)
63
+ ### [Concurrency groups](https://docs.kici.dev/user/concurrency/)
64
64
 
65
65
  Control parallel execution with auto-cancel and queue modes. Prevent multiple workflow runs from executing in parallel when they target the same resource.
66
66
 
67
- ### [Dynamic values](dynamic-values.md)
67
+ ### [Dynamic values](https://docs.kici.dev/user/dynamic-values/)
68
68
 
69
69
  Compute `environment`, `env`, and `concurrencyGroup` at runtime based on the incoming event payload. Instead of hardcoding static strings, pass a function that receives the webhook event and returns the resolved value.
70
70
 
71
- ### [Secrets](secrets.md)
71
+ ### [Secrets](https://docs.kici.dev/user/secrets/)
72
72
 
73
73
  Access encrypted secrets in workflow steps via the explicit secrets API. Secrets are never auto-injected into `process.env` -- you must explicitly request each secret by name.
74
74
 
75
- ### [GitHub App provider](providers/github.md)
75
+ ### [GitHub App provider](https://docs.kici.dev/user/providers/github/)
76
76
 
77
77
  The flagship source. Covers creating the GitHub App on GitHub's side (permissions, webhook URL, private key), registering it with the orchestrator via `kici-admin source add github`, routing keys (`github:<appId>`), global-workflow policy, enriched Check runs on pull requests, private-key and webhook-secret rotation, and troubleshooting.
78
78
 
79
- ### [Universal-git provider](providers/universal-git.md)
79
+ ### [Universal-git provider](https://docs.kici.dev/user/providers/universal-git/)
80
80
 
81
81
  Connect a non-GitHub-App forge (Forgejo, Gitea, Gogs, GitLab, plain GitHub) to KiCI via its webhook. Covers preset selection, PAT and SSH credential wiring, credential rotation, global workflow policy against `generic:<orgId>:<sourceId>` routing keys, and troubleshooting.
82
82
 
83
- ### [Global workflows](global-workflows.md)
83
+ ### [Global workflows](https://docs.kici.dev/user/global-workflows/)
84
84
 
85
85
  Cross-repo workflows that let a single workflow repo define jobs which run on events from many source repos in the same org. Covers the mental model (workflow repo vs. source repo, authoring axis vs. source axis), SDK syntax for declaring globals via `repos:` patterns, the dashboard opt-in flow and per-setting semantics (master toggle, author allow-list, source deny-list, elevated-access list), the security model, and troubleshooting skipped dispatches.
86
86
 
@@ -136,7 +136,7 @@ npx kici init --mjs
136
136
 
137
137
  This creates `.mjs` workflow files that run directly without a build step.
138
138
 
139
- After running `kici init`, jump straight to [Compile the workflow](#compile-the-workflow) below to compile and preview the scaffolded workflow.
139
+ After running `kici init`, jump straight to [Compile the workflow](https://docs.kici.dev/user/getting-started/#compile-the-workflow) below to compile and preview the scaffolded workflow.
140
140
 
141
141
  ## Manual setup
142
142
 
@@ -215,7 +215,7 @@ 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.
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](https://docs.kici.dev/user/sdk/core/#runson-forms) reference for the full label model.
219
219
 
220
220
  **Single-step shortcut.** If a job only has one step, pass `run` directly to `job()` instead of building a `steps: [step(...)]` array:
221
221
 
@@ -229,7 +229,7 @@ const deploy = job('deploy', {
229
229
  });
230
230
  ```
231
231
 
232
- `run` and `steps` are mutually exclusive. The shorthand is ideal for deploy/notify/smoke-test jobs. See [Single-step job shorthand](sdk/core.md#single-step-job-shorthand) in the SDK reference for details (output access on the resulting `job.result` is flat -- no step-name nesting).
232
+ `run` and `steps` are mutually exclusive. The shorthand is ideal for deploy/notify/smoke-test jobs. See [Single-step job shorthand](https://docs.kici.dev/user/sdk/core/#single-step-job-shorthand) in the SDK reference for details (output access on the resulting `job.result` is flat -- no step-name nesting).
233
233
 
234
234
  ## Compile the workflow
235
235
 
@@ -245,14 +245,14 @@ Expected output:
245
245
  ✓ Compiled workflows → .kici/kici.lock.json (1 workflow)
246
246
  ```
247
247
 
248
- The lock file (`kici.lock.json`) is a JSON representation of your workflow that the KiCI agent uses for execution. Commit this file alongside your workflow source. See [Lock file and workflow drift](lock-file-and-drift.md) for why and how to keep them in sync.
248
+ The lock file (`kici.lock.json`) is a JSON representation of your workflow that the KiCI agent uses for execution. Commit this file alongside your workflow source. See [Lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/) for why and how to keep them in sync.
249
249
 
250
250
  ## Preview trigger matching
251
251
 
252
- Use `kici test` to preview which workflows match a trigger event (dry-run, no execution):
252
+ Use `kici preview` to preview which workflows match a trigger event (dry-run, no execution):
253
253
 
254
254
  ```bash
255
- npx kici test pr:open
255
+ npx kici preview pr:open
256
256
  ```
257
257
 
258
258
  Expected output (simplified):
@@ -316,7 +316,7 @@ Every `.kici/` dependency must be resolvable from the **single cloned repository
316
316
 
317
317
  In practice:
318
318
 
319
- - **From a registry** — the common case. Pin a published version (a private registry works — see [Private registries](./private-registries.md)). Available for any package manager.
319
+ - **From a registry** — the common case. Pin a published version (a private registry works — see [Private registries](https://docs.kici.dev/user/private-registries/)). Available for any package manager.
320
320
  - **From an in-repo workspace sibling** — if your `.kici/` is a member of a **pnpm workspace** or a **yarn berry workspace** (a `workspaces` array in the repo-root `package.json`), it can depend on a sibling package in the same repo via `workspace:*` (yarn berry also accepts `portal:`). The whole repo is cloned, so the sibling is present and resolves; the agent also builds your `.kici/` dependency closure after install, so a sibling's build output exists before the workflow that imports it loads. A `file:`/`link:`/`portal:` path is allowed only when it stays inside the repository.
321
321
 
322
322
  What fails fast (with an actionable error naming the dependency, not a raw package-manager error): a `workspace:` dependency in an **npm** project (npm has no workspace protocol — pin a published version or switch to pnpm), a `workspace:`/`portal:` dependency in a **yarn classic** project (v1 has neither — use a version range, pnpm, or yarn berry), a `workspace:` dependency in a **yarn berry** project whose repo-root `package.json` has no `workspaces` array, and any `file:`/`link:`/`portal:` path that points outside the cloned repo.
@@ -418,14 +418,14 @@ Or add the flag to your root `package.json`:
418
418
 
419
419
  ## Authoring KiCI workflows with LLM coding agents
420
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.
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 preview` 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
422
 
423
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:
424
424
 
425
425
  - where the SDK type declarations live (`node_modules/@kici-dev/sdk/dist/index.d.ts`)
426
426
  - the five canonical authoring patterns with runnable examples
427
427
  - the anti-patterns that catch agents off-guard (no YAML, no `/dist/...` imports, no top-level `await`)
428
- - the local commands the agent should drive (`kici compile --check`, `kici test`, `kici run local`, `kici docs llm`)
428
+ - the local commands the agent should drive (`kici compile --check`, `kici preview`, `kici run local`, `kici docs llm`)
429
429
 
430
430
  If you don't want the file, pass `--no-agents-md` to `kici init`, or delete the file afterwards — KiCI never reads it at runtime.
431
431
 
@@ -450,10 +450,10 @@ The compiler watches `.kici/workflows/*.ts` and recompiles on every save.
450
450
 
451
451
  ## Next steps
452
452
 
453
- - **[5-minute quickstart](quickstart.md)** -- ready to run your workflow on real infrastructure? Stand up an orchestrator + agent (Docker / Podman or bare metal)
454
- - **[SDK reference](sdk-reference.md)** -- complete API for workflows, jobs, steps, triggers, rules, and matrix
455
- - **[CLI reference](cli-reference.md)** -- all CLI commands with options and examples
456
- - **[Workflow patterns](workflow-patterns.md)** -- common patterns for real-world CI/CD workflows
453
+ - **[5-minute quickstart](https://docs.kici.dev/user/quickstart/)** -- ready to run your workflow on real infrastructure? Stand up an orchestrator + agent (Docker / Podman or bare metal)
454
+ - **[SDK reference](https://docs.kici.dev/user/sdk-reference/)** -- complete API for workflows, jobs, steps, triggers, rules, and matrix
455
+ - **[CLI reference](https://docs.kici.dev/user/cli-reference/)** -- all CLI commands with options and examples
456
+ - **[Workflow patterns](https://docs.kici.dev/user/workflow-patterns/)** -- common patterns for real-world CI/CD workflows
457
457
 
458
458
  ## How KiCI works
459
459
 
@@ -475,10 +475,10 @@ The lock file approach means the orchestrator stays git-agnostic -- it only need
475
475
 
476
476
  ## See also
477
477
 
478
- - [SDK reference](sdk-reference.md) -- complete API for workflows, jobs, steps, triggers, rules, and matrix
479
- - [CLI reference](cli-reference.md) -- all CLI commands with options and examples
480
- - [Workflow patterns](workflow-patterns.md) -- common patterns for real-world CI/CD workflows
481
- - [Architecture overview](../architecture/overview.md) -- how the three-tier runtime executes your workflows
478
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) -- complete API for workflows, jobs, steps, triggers, rules, and matrix
479
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/) -- all CLI commands with options and examples
480
+ - [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/) -- common patterns for real-world CI/CD workflows
481
+ - [Architecture overview](https://docs.kici.dev/architecture/overview/) -- how the three-tier runtime executes your workflows
482
482
 
483
483
  ---
484
484
 
@@ -494,13 +494,13 @@ Each guide is split into two parts. **Part 1** gets you a green run against your
494
494
 
495
495
  Two containers brought up with `docker compose up -d` (orchestrator + PostgreSQL), plus one short-lived agent container spawned per job by the container scaler. Minimal host setup, perfect for a laptop, home server, or a tiny VM. No need to install PostgreSQL or any other system service.
496
496
 
497
- [Start with the Docker / Podman quickstart →](./quickstart/compose.md)
497
+ [Start with the Docker / Podman quickstart →](https://docs.kici.dev/user/quickstart/compose/)
498
498
 
499
499
  ## Option B — Bare-metal install
500
500
 
501
501
  Native systemd services managed by `kici-admin orchestrator install` / `kici-admin agent install` — the orchestrator and agents run as native processes. The backing PostgreSQL runs as a single container by default (one `docker compose up -d`), or you can install it natively if you'd rather not run a container runtime at all. Best for a long-lived Linux host.
502
502
 
503
- [Start with the bare-metal quickstart →](./quickstart/bare-metal.md)
503
+ [Start with the bare-metal quickstart →](https://docs.kici.dev/user/quickstart/bare-metal/)
504
504
 
505
505
  ## Which should I pick?
506
506
 
@@ -516,7 +516,7 @@ If you're not sure, pick Docker / Podman.
516
516
 
517
517
  ## Looking for the laptop-only path?
518
518
 
519
- Both quickstarts deploy a real orchestrator + agent. If you only want to write a workflow and dry-run it on your laptop with no infrastructure, [Getting started](./getting-started.md) covers `kici test` and `kici run local` instead.
519
+ Both quickstarts deploy a real orchestrator + agent. If you only want to write a workflow and dry-run it on your laptop with no infrastructure, [Getting started](https://docs.kici.dev/user/getting-started/) covers `kici preview` and `kici run local` instead.
520
520
 
521
521
  ---
522
522
 
@@ -576,7 +576,7 @@ export default workflow('ci', {
576
576
  });
577
577
  ```
578
578
 
579
- The `test` and `typecheck` jobs both depend on `lint`, so they run in parallel after lint succeeds. KiCI validates the dependency graph at compile time -- cycles and missing references are caught before you commit. At runtime, jobs are gated on upstream completion: a job only dispatches after every entry in its `needs` array reaches a terminal status that satisfies the edge. If an upstream fails, downstream jobs skip by default (override per-edge with `when: 'always'`). See [Job dependencies (`needs`)](../sdk/core.md#job-dependencies-needs) in the SDK reference for the full matrix of `needs` forms (string, `Job` ref, `{ name, when }`, `dynamicGroup()`) and [needs-scheduler](../../architecture/execution/needs-scheduler.md) for the dispatch semantics.
579
+ The `test` and `typecheck` jobs both depend on `lint`, so they run in parallel after lint succeeds. KiCI validates the dependency graph at compile time -- cycles and missing references are caught before you commit. At runtime, jobs are gated on upstream completion: a job only dispatches after every entry in its `needs` array reaches a terminal status that satisfies the edge. If an upstream fails, downstream jobs skip by default (override per-edge with `when: 'always'`). See [Job dependencies (`needs`)](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs) in the SDK reference for the full matrix of `needs` forms (string, `Job` ref, `{ name, when }`, `dynamicGroup()`) and [needs-scheduler](https://docs.kici.dev/architecture/execution/needs-scheduler/) for the dispatch semantics.
580
580
 
581
581
  **Single-step jobs don't need a `steps` array.** When a job only does one thing, pass `run` to `job()` instead of wrapping it in `steps: [step(...)]`:
582
582
 
@@ -597,7 +597,7 @@ export default workflow('smoke', {
597
597
  });
598
598
  ```
599
599
 
600
- `run` is mutually exclusive with `steps` (throws at compile time if both are set). Outputs are flat on `job.result` (no step-name nesting). See [Single-step job shorthand](../sdk/core.md#single-step-job-shorthand) in the SDK reference.
600
+ `run` is mutually exclusive with `steps` (throws at compile time if both are set). Outputs are flat on `job.result` (no step-name nesting). See [Single-step job shorthand](https://docs.kici.dev/user/sdk/core/#single-step-job-shorthand) in the SDK reference.
601
601
 
602
602
  ## PR-only workflow with branch filters
603
603
 
@@ -744,7 +744,7 @@ npx kici compile # regenerate .kici/kici.lock.json
744
744
  npx kici run local dispatch
745
745
  ```
746
746
 
747
- `kici run local` compiles the workflow, matches triggers against a simulated `dispatch` event, and executes the matched jobs directly on your machine with DAG-based scheduling. No webhook, no GitHub, no deployed orchestrator involved. See [`kici run local`](../cli-reference.md#kici-run-local) for options like `--job`, `--env`, `--json`, and `--junit`.
747
+ `kici run local` compiles the workflow, matches triggers against a simulated `dispatch` event, and executes the matched jobs directly on your machine with DAG-based scheduling. No webhook, no GitHub, no deployed orchestrator involved. See [`kici run local`](https://docs.kici.dev/user/cli-reference/#kici-run-local) for options like `--job`, `--env`, `--json`, and `--junit`.
748
748
 
749
749
  ### Scoping to a single workflow
750
750
 
@@ -1267,7 +1267,7 @@ export const postDeploy = workflow('post-deploy', {
1267
1267
 
1268
1268
  ### Same-run discovery → fan-out
1269
1269
 
1270
- A result-aware [`dynamicJob(group, { needs, generate })`](../sdk/rules-matrix-dynamic.md#dynamicjob--result-aware-generation) is deferred until its declared upstreams complete, then runs with their frozen outputs as `ctx.needs` — so a discovery job can emit a list at runtime and the generator fans out one follow-up job per item, all in the same run:
1270
+ A result-aware [`dynamicJob(group, { needs, generate })`](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/#dynamicjob--result-aware-generation) is deferred until its declared upstreams complete, then runs with their frozen outputs as `ctx.needs` — so a discovery job can emit a list at runtime and the generator fans out one follow-up job per item, all in the same run:
1271
1271
 
1272
1272
  ```typescript
1273
1273
  import { workflow, job, step, push, dynamicJob, z } from '@kici-dev/sdk';
@@ -1373,7 +1373,7 @@ export default workflow('on-argocd-deploy', {
1373
1373
  });
1374
1374
  ```
1375
1375
 
1376
- See the [Operator guide: event routing](../../operator/event-routing.md) for how to set up generic webhook sources, verification methods, and trust relationships.
1376
+ See the [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) for how to set up generic webhook sources, verification methods, and trust relationships.
1377
1377
 
1378
1378
  ## Stripe webhook handler
1379
1379
 
@@ -1415,9 +1415,9 @@ export default workflow('stripe-invoice-handler', {
1415
1415
 
1416
1416
  **Prerequisites:**
1417
1417
 
1418
- - An operator must create a generic webhook source named `stripe` via the admin API. See [Operator guide: creating a source](../../operator/event-routing.md#creating-a-source).
1418
+ - An operator must create a generic webhook source named `stripe` via the admin API. See [Operator guide: creating a source](https://docs.kici.dev/operator/event-routing/#creating-a-source).
1419
1419
  - The `stripe-signing-key` secret must contain your Stripe webhook signing secret.
1420
- - This workflow uses the [registration model](../events.md#the-registration-model) -- it will not trigger until you push to your default branch.
1420
+ - This workflow uses the [registration model](https://docs.kici.dev/user/events/#the-registration-model) -- it will not trigger until you push to your default branch.
1421
1421
 
1422
1422
  ## Self-hosted git forge (Gogs, Forgejo, Gitea)
1423
1423
 
@@ -1466,7 +1466,7 @@ export default workflow('on-forgejo-push', {
1466
1466
 
1467
1467
  **Caveat — cloning:** generic webhook sources deliver only the payload; they do not carry a clone token, and KiCI's automatic pre-step clone (`packages/agent/src/checkout/git-clone.ts`) is GitHub-only today (HTTPS + `http.extraHeader` Basic auth with a GitHub installation token). Three practical patterns:
1468
1468
 
1469
- - **Mirror to GitHub and fan out.** Keep the repo on GitHub, register the workflow via a GitHub default-branch push, and have Gogs/Forgejo webhooks fan out via [cross-source delivery](../../architecture/webhooks/webhook-delivery.md#cross-source-delivery). The clone runs against the GitHub mirror using the GitHub App's token.
1469
+ - **Mirror to GitHub and fan out.** Keep the repo on GitHub, register the workflow via a GitHub default-branch push, and have Gogs/Forgejo webhooks fan out via [cross-source delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/#cross-source-delivery). The clone runs against the GitHub mirror using the GitHub App's token.
1470
1470
  - **Clone yourself using a secret.** Set `checkout: false` on the job to skip the framework clone, store an SSH private key or forge personal access token as a secret, and run `git clone` explicitly in the first step. This works for any forge the agent can reach, no mirror needed. You still need a way to **register** the workflow — either keep a one-file GitHub repo whose only job is to own the registration, or bootstrap the registration manually against the orchestrator DB.
1471
1471
  - **Self-contained workflow.** No clone at all. The step reads whatever it needs from `rawPayload` (e.g., `rawPayload.after`, `rawPayload.repository.clone_url`) and drives external systems — notifications, deploys, third-party CI triggers.
1472
1472
 
@@ -1500,7 +1500,7 @@ HTTPS with a forge PAT works the same way — store the token as a secret, `awai
1500
1500
 
1501
1501
  - An operator must create a generic webhook source via `kici-admin source add generic` (see above).
1502
1502
  - The forge's webhook secret must match the `--secret` value.
1503
- - The workflow uses the [registration model](../events.md#the-registration-model) -- push to the default branch of a registered repo before the first webhook fires.
1503
+ - The workflow uses the [registration model](https://docs.kici.dev/user/events/#the-registration-model) -- push to the default branch of a registered repo before the first webhook fires.
1504
1504
 
1505
1505
  ## Plain GitHub repo webhooks (no GitHub App)
1506
1506
 
@@ -1562,12 +1562,12 @@ export default workflow('on-github-repo-push', {
1562
1562
  **What you lose compared to the GitHub App** (these are the same cloning / metadata caveats that apply to the Gogs/Forgejo pattern, plus GitHub-specific integrations):
1563
1563
 
1564
1564
  - No auto-clone — `packages/agent/src/checkout/git-clone.ts` uses GitHub App installation tokens to fetch the repo; a generic source has none. Either set `checkout: false` and clone yourself with a PAT/Deploy Key secret (same pattern as the Forgejo manual-clone example above), or keep the workflow self-contained.
1565
- - No lock-file fetch — the orchestrator cannot fetch `.kici/kici.lock.json` at the pushed SHA via the GitHub API. The workflow must be pre-registered via the [registration model](../events.md#the-registration-model); ad-hoc per-commit workflow discovery that a GitHub App push gives you is not available.
1565
+ - No lock-file fetch — the orchestrator cannot fetch `.kici/kici.lock.json` at the pushed SHA via the GitHub API. The workflow must be pre-registered via the [registration model](https://docs.kici.dev/user/events/#the-registration-model); ad-hoc per-commit workflow discovery that a GitHub App push gives you is not available.
1566
1566
  - No changed-files enrichment — `event.changedFiles` is empty. Use JSONPath `match` on `rawPayload.commits[*].added/modified/removed` if you need path filters.
1567
1567
  - No check-run integration — KiCI cannot post Check Run results back to GitHub.
1568
1568
  - Workflow authors must use `genericWebhook()`, not `push()` / `pr()` / `webhook()` — the latter three only match events delivered through the native GitHub App provider.
1569
1569
 
1570
- **When to use it anyway:** trigger-only workflows that don't need the cloned repo — posting Slack messages, kicking off external deploys, forwarding to downstream systems, or exposing GitHub repo events as `genericWebhook` for same-org [cross-source fan-out](../../architecture/webhooks/webhook-delivery.md#cross-source-delivery). For anything that compiles, tests, or checks code, install the GitHub App instead.
1570
+ **When to use it anyway:** trigger-only workflows that don't need the cloned repo — posting Slack messages, kicking off external deploys, forwarding to downstream systems, or exposing GitHub repo events as `genericWebhook` for same-org [cross-source fan-out](https://docs.kici.dev/architecture/webhooks/webhook-delivery/#cross-source-delivery). For anything that compiles, tests, or checks code, install the GitHub App instead.
1571
1571
 
1572
1572
  ## Nightly cron build
1573
1573
 
@@ -1700,7 +1700,7 @@ export default workflow('nightly-build', {
1700
1700
  **Notes:**
1701
1701
 
1702
1702
  - The `cron` field uses standard 5-field cron syntax. Use the `timezone` option (defaults to `'UTC'`) to control evaluation in a specific timezone: `schedule({ cron: '0 2 * * *', timezone: 'America/New_York' })`.
1703
- - Schedule workflows use the [registration model](../events.md#the-registration-model) -- the cron will not start firing until you push to your default branch.
1703
+ - Schedule workflows use the [registration model](https://docs.kici.dev/user/events/#the-registration-model) -- the cron will not start firing until you push to your default branch.
1704
1704
  - In clustered orchestrator deployments, only the Raft leader evaluates cron schedules. If the leader changes, the new leader recovers missed schedules.
1705
1705
 
1706
1706
  **Timing precision and scaling:**
@@ -1764,7 +1764,7 @@ export const deploy = workflow('deploy-on-success', {
1764
1764
 
1765
1765
  - `workflowComplete()` is a system event trigger -- the orchestrator automatically emits these events when workflows finish. You do not need to call `ctx.emit()`.
1766
1766
  - The `status` filter accepts `'success'`, `'failed'`, and `'cancelled'`. Omit `status` to trigger on any completion.
1767
- - The `deploy-on-success` workflow uses the [registration model](../events.md#the-registration-model) -- it will not trigger until you push to your default branch. The `build` workflow (using `push()`) works immediately.
1767
+ - The `deploy-on-success` workflow uses the [registration model](https://docs.kici.dev/user/events/#the-registration-model) -- it will not trigger until you push to your default branch. The `build` workflow (using `push()`) works immediately.
1768
1768
  - You can also use `jobComplete()` to trigger on individual job completions within a workflow.
1769
1769
 
1770
1770
  ## Custom event chaining
@@ -1836,8 +1836,8 @@ export const autoDeploy = workflow('auto-deploy', {
1836
1836
  - `defineEvent()` creates a typed contract using Zod. This is optional but recommended for documenting event payloads.
1837
1837
  - Custom events are delivered immediately when `ctx.emit()` is called (mid-workflow), not queued until the workflow completes.
1838
1838
  - Payload matching is available via the `match` option: `kiciEvent({ name: 'tests-passed', match: { '$.branch': 'main' } })`.
1839
- - The `auto-deploy` workflow uses the [registration model](../events.md#the-registration-model) -- it will not trigger until you push to your default branch.
1840
- - The [circuit breaker](../events.md#circuit-breaker) limits chain depth (default: 10) and rate (default: 100/min per workflow) to prevent infinite loops.
1839
+ - The `auto-deploy` workflow uses the [registration model](https://docs.kici.dev/user/events/#the-registration-model) -- it will not trigger until you push to your default branch.
1840
+ - The [circuit breaker](https://docs.kici.dev/user/events/#circuit-breaker) limits chain depth (default: 10) and rate (default: 100/min per workflow) to prevent infinite loops.
1841
1841
 
1842
1842
  ## Step context
1843
1843
 
@@ -1856,7 +1856,7 @@ Two surfaces drive the same cache:
1856
1856
  - **Declarative** — a `cache` field on a job or a step. The runtime restores before the work runs and saves after it succeeds, with no code in your step body.
1857
1857
  - **Imperative** — `ctx.cache.restore(spec)` / `ctx.cache.save(spec)` inside a step body, for fine-grained control over when restore and save happen.
1858
1858
 
1859
- The cache is backed by the orchestrator's object storage. Entries are isolated per organization and per ref scope (see [Isolation](#isolation)); no other tenant can read your cache, and an untrusted/fork ref can never poison the cache a trusted branch reads.
1859
+ The cache is backed by the orchestrator's object storage. Entries are isolated per organization and per ref scope (see [Isolation](https://docs.kici.dev/user/sdk/caching/#isolation)); no other tenant can read your cache, and an untrusted/fork ref can never poison the cache a trusted branch reads.
1860
1860
 
1861
1861
  ## CacheSpec
1862
1862
 
@@ -1973,18 +1973,18 @@ Cache storage is bounded per organization. Two mechanisms keep it bounded:
1973
1973
  - **Quota** — when a save pushes the org over its byte quota (`KICI_USER_CACHE_QUOTA_BYTES`, default 5 GiB), the oldest entries are evicted until the org is back under quota.
1974
1974
  - **TTL** — entries unused for `KICI_USER_CACHE_TTL_MS` (default 7 days) expire. The TTL refreshes on read (touch-on-read), so an actively used cache stays warm.
1975
1975
 
1976
- Both knobs are operator-configured on the orchestrator — see [orchestrator storage layout](../../operator/orchestrator/storage-layout.md).
1976
+ Both knobs are operator-configured on the orchestrator — see [orchestrator storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/).
1977
1977
 
1978
1978
  ## Observability
1979
1979
 
1980
- Each cache restore and save surfaces in the run timeline as a `cache:restore` / `cache:save` pseudo-step, reporting the outcome (hit/miss/saved, the matched key, bytes). The same outcomes are recorded as `cache.restore` / `cache.save` run events. See [data flows](../../architecture/data-flows.md#user-facing-cache-flow) for the restore/save protocol.
1980
+ Each cache restore and save surfaces in the run timeline as a `cache:restore` / `cache:save` pseudo-step, reporting the outcome (hit/miss/saved, the matched key, bytes). The same outcomes are recorded as `cache.restore` / `cache.save` run events. See [data flows](https://docs.kici.dev/architecture/data-flows/#user-facing-cache-flow) for the restore/save protocol.
1981
1981
 
1982
1982
  ## See also
1983
1983
 
1984
- - [Core](./core.md) -- `job()` / `step()` factories the `cache` field attaches to
1985
- - [Runtime](./runtime.md) -- `StepContext`, where `ctx.cache` lives
1986
- - [Orchestrator storage layout](../../operator/orchestrator/storage-layout.md) -- cache prefix, quota, TTL, and eviction
1987
- - [Data flows](../../architecture/data-flows.md#user-facing-cache-flow) -- restore/save protocol and trust→scope mapping
1984
+ - [Core](https://docs.kici.dev/user/sdk/core/) -- `job()` / `step()` factories the `cache` field attaches to
1985
+ - [Runtime](https://docs.kici.dev/user/sdk/runtime/) -- `StepContext`, where `ctx.cache` lives
1986
+ - [Orchestrator storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/) -- cache prefix, quota, TTL, and eviction
1987
+ - [Data flows](https://docs.kici.dev/architecture/data-flows/#user-facing-cache-flow) -- restore/save protocol and trust→scope mapping
1988
1988
 
1989
1989
  ---
1990
1990
 
@@ -2018,8 +2018,8 @@ function workflow(name: string, options: WorkflowOptions): Workflow;
2018
2018
  | `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel) |
2019
2019
  | `options.onSuccess` | `HookInput` | no | Runs on workflow success |
2020
2020
  | `options.onFailure` | `HookInput` | no | Runs on workflow failure |
2021
- | `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](../concurrency.md). |
2022
- | `options.timeout` | `number` | no | Whole-run wall-clock timeout in milliseconds across all jobs. On breach the orchestrator cancels the run and marks it timed out. See [Timeouts](#timeouts). |
2021
+ | `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](https://docs.kici.dev/user/concurrency/). |
2022
+ | `options.timeout` | `number` | no | Whole-run wall-clock timeout in milliseconds across all jobs. On breach the orchestrator cancels the run and marks it timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
2023
2023
 
2024
2024
  **Returns:** `Workflow` -- an immutable workflow definition.
2025
2025
 
@@ -2032,7 +2032,7 @@ export default workflow('ci', {
2032
2032
  });
2033
2033
  ```
2034
2034
 
2035
- Secret scoping happens at the job level via `environment` (see [job options](#jobname-options--joboptions) and [Secrets](../secrets.md)) — the workflow itself does not declare which secret environments it can read.
2035
+ Secret scoping happens at the job level via `environment` (see [job options](https://docs.kici.dev/user/sdk/core/#jobname-options--joboptions) and [Secrets](https://docs.kici.dev/user/secrets/)) — the workflow itself does not declare which secret environments it can read.
2036
2036
 
2037
2037
  ### job(name, options) / job(options)
2038
2038
 
@@ -2050,8 +2050,8 @@ function job(options: JobOptions): Job;
2050
2050
  | `name` | `string` | no | Job name (auto-generated UUID if omitted) |
2051
2051
  | `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
2052
2052
  | `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
2053
- | `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`. |
2054
- | `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](#job-dependencies-needs) |
2053
+ | `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](https://docs.kici.dev/user/sdk/core/#single-step-job-shorthand). Mutually exclusive with `steps`. |
2054
+ | `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs) |
2055
2055
  | `options.rules` | `Rule[]` | no | Conditions for conditional execution |
2056
2056
  | `options.description` | `string` | no | Human-readable description |
2057
2057
  | `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
@@ -2059,19 +2059,19 @@ function job(options: JobOptions): Job;
2059
2059
  | `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
2060
2060
  | `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
2061
2061
  | `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. |
2062
- | `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). |
2063
- | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
2064
- | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](../concurrency.md). |
2062
+ | `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
2063
+ | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
2064
+ | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](https://docs.kici.dev/user/concurrency/). |
2065
2065
  | `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
2066
2066
  | `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
2067
2067
  | `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
2068
2068
  | `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
2069
2069
  | `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
2070
2070
  | `options.afterStep` | `HookInput` | no | Hook that runs after each step |
2071
- | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](../hooks.md#hook-timeout). |
2072
- | `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). |
2073
- | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](#per-job-resources) below. |
2074
- | `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. |
2071
+ | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](https://docs.kici.dev/user/hooks/#hook-timeout). |
2072
+ | `options.timeout` | `number` | no | Total job wall-clock timeout in milliseconds (init + all steps + hooks). On breach the job is aborted and reported timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
2073
+ | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](https://docs.kici.dev/user/sdk/core/#per-job-resources) below. |
2074
+ | `options.init` | `InitConfig` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. A generic config, a typed preset (`'mise'` / `{ mise }`), `'auto'`, or `false`. See [Per-job init](https://docs.kici.dev/user/sdk/core/#per-job-init) below. |
2075
2075
 
2076
2076
  **Returns:** `Job` -- an immutable job definition.
2077
2077
 
@@ -2092,7 +2092,7 @@ const build = job({
2092
2092
 
2093
2093
  #### runsOn forms
2094
2094
 
2095
- 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, and each label can be an exact string, a glob, or a regular expression (see [Targeting by pattern](#targeting-by-pattern) below):
2095
+ 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, and each label can be an exact string, a glob, or a regular expression (see [Targeting by pattern](https://docs.kici.dev/user/sdk/core/#targeting-by-pattern) below):
2096
2096
 
2097
2097
  ```typescript
2098
2098
  // 1. Simple string -- agent must have this label
@@ -2118,7 +2118,7 @@ runsOn: { labels: ['kici:os:linux'], exclude: ['kici:host:box-01'] }
2118
2118
  - **Required labels:** The agent must have every label in the `labels` array (or the string/array form).
2119
2119
  - **Excluded labels:** The agent must NOT have any label in the `exclude` array. This includes auto-derived labels like `kici:arch:arm64`, `kici:os:linux`, etc.
2120
2120
  - **Compile-time validation:** The compiler will error if any label appears in both `labels` and `exclude` (overlap detection).
2121
- - **Operator-declared mandatory labels:** Operators may mark a scaler with `mandatoryLabels` (Kubernetes-taint-style opt-in). When a scaler declares a mandatory label, a job is only allowed to land on it if `runsOn.labels` includes that label. A workflow targeting such a scaler must explicitly list the mandatory label in `runsOn`. See the [auto-scaler mandatory labels](../../operator/orchestrator/auto-scaler/common-config.md#mandatory--exclude-labels) for details.
2121
+ - **Operator-declared mandatory labels:** Operators may mark a scaler with `mandatoryLabels` (Kubernetes-taint-style opt-in). When a scaler declares a mandatory label, a job is only allowed to land on it if `runsOn.labels` includes that label. A workflow targeting such a scaler must explicitly list the mandatory label in `runsOn`. See the [auto-scaler mandatory labels](https://docs.kici.dev/operator/orchestrator/auto-scaler/common-config/#mandatory--exclude-labels) for details.
2122
2122
 
2123
2123
  ```typescript
2124
2124
  // Route to any Linux agent that does NOT have the 'gpu' label
@@ -2134,6 +2134,33 @@ const deploy = job('deploy', {
2134
2134
  });
2135
2135
  ```
2136
2136
 
2137
+ #### Single-host selection: `pick`
2138
+
2139
+ When more than one agent matches a `runsOn` selector, the object form's `pick`
2140
+ field controls **which** one runs the job:
2141
+
2142
+ ```typescript
2143
+ // Always the same host across re-runs (default — can be omitted)
2144
+ runsOn: { labels: ['role:db'], pick: 'deterministic' }
2145
+
2146
+ // Any available host (load spread)
2147
+ runsOn: { labels: ['role:db'], pick: 'any' }
2148
+ ```
2149
+
2150
+ - **`'deterministic'` (the default)** — the orchestrator sorts the matching
2151
+ agents by their agent id and picks the lowest. A job that must run exactly
2152
+ once on one stable host — a database migration, a backup dump — lands on the
2153
+ **same** host every run. The string and array shorthand forms
2154
+ (`runsOn: 'role:db'`, `runsOn: ['role:db', 'linux']`) inherit this default.
2155
+ - **`'any'`** — pick any available matching agent. Use this for jobs that don't
2156
+ need a stable host and benefit from spreading load across an equivalent pool.
2157
+
2158
+ **Trade-off:** `'deterministic'` can hot-spot — if many jobs target the same
2159
+ label set, they all pin to the same lowest-id agent. Use `'any'` to spread those
2160
+ across the pool; keep `'deterministic'` when reproducibility matters more than
2161
+ balance. (`pick` selects among **single-agent** candidates; to fan a job out to
2162
+ **every** matching host, use [`runsOnAll`](https://docs.kici.dev/user/sdk/runs-on-all/) instead.)
2163
+
2137
2164
  #### Targeting by pattern
2138
2165
 
2139
2166
  Every selector element — in `runsOn`, in `runsOnAll`, on both the include and the exclude side — can be a plain string, a glob pattern, or a regular expression. KiCI picks the matching mode from the value itself:
@@ -2238,7 +2265,7 @@ const configureNginx = step('configure-nginx', {
2238
2265
  ```
2239
2266
 
2240
2267
  A checked step can run in apply mode (converge) or `--check` preview mode (report
2241
- drift, change nothing). See [Idempotent steps and check mode](../idempotent-steps.md).
2268
+ drift, change nothing). See [Idempotent steps and check mode](https://docs.kici.dev/user/idempotent-steps/).
2242
2269
 
2243
2270
  ### Per-job resources
2244
2271
 
@@ -2321,7 +2348,7 @@ export const build = workflow('build', {
2321
2348
  | --------- | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
2322
2349
  | `run` | `string` | yes | Command run after clone, before steps. Runs in the job's sandbox at the clone root. Must be a non-empty command. |
2323
2350
  | `shell` | `string` | no | Shell used to run `run`. Defaults to `bash`. |
2324
- | `cache` | `CacheSpec` | no | Cache spec for binaries the command installs -- restored before the command, saved after on a key miss. See [Caching](./caching.md). |
2351
+ | `cache` | `CacheSpec` | no | Cache spec for binaries the command installs -- restored before the command, saved after on a key miss. See [Caching](https://docs.kici.dev/user/sdk/caching/). |
2325
2352
  | `timeout` | `number` | no | Max wall-clock for this init command in milliseconds. Defaults to 10 minutes. On breach the init is aborted and the job is reported timed out. |
2326
2353
  | `env` | `Record<string,string>` | no | Static environment variables available to the command. |
2327
2354
 
@@ -2504,6 +2531,43 @@ export default workflow('ci', {
2504
2531
 
2505
2532
  Workflow and job timeouts surface with a distinct "timed out" reason so the dashboard labels the run or job as timed out rather than a generic failure or cancel.
2506
2533
 
2534
+ ### Retries
2535
+
2536
+ A step can declare a `retry` policy so a thrown attempt is re-run automatically instead of failing the job on the first error. Use it for genuinely transient failures — a flaky network call, an occasional 503, a dependency that is briefly not ready.
2537
+
2538
+ ```typescript
2539
+ step('publish', {
2540
+ retry: 3, // shorthand for { maxAttempts: 3 } with the defaults below
2541
+ run: async (ctx) => {
2542
+ await ctx.$`pnpm publish`;
2543
+ },
2544
+ });
2545
+
2546
+ step('fetch-token', {
2547
+ retry: {
2548
+ maxAttempts: 5, // total attempts including the first; must be >= 1
2549
+ delayMs: 500, // base delay between attempts (default 1000)
2550
+ backoff: 'exponential', // 'exponential' (default) or 'fixed'
2551
+ maxDelayMs: 30_000, // cap for exponential growth (default 30000)
2552
+ retryIf: (err) => err instanceof TransientError, // default: retry on any throw
2553
+ },
2554
+ run: async (ctx) => {
2555
+ await fetchToken();
2556
+ },
2557
+ });
2558
+ ```
2559
+
2560
+ - **`retry: N`** is shorthand for `{ maxAttempts: N }` with all defaults applied.
2561
+ - **Defaults:** `delayMs: 1000`, `backoff: 'exponential'`, `maxDelayMs: 30000`, and "retry on any throw" when no `retryIf` is given.
2562
+ - **Backoff.** With `'exponential'`, the wait after the `n`-th attempt (1-based) is `min(delayMs * 2 ** (n - 1), maxDelayMs)` — 1s, 2s, 4s, … capped at `maxDelayMs`. With `'fixed'`, the wait is always `delayMs`.
2563
+ - **`retryIf(err)`** runs against the thrown error before each retry; return `false` to stop retrying immediately and let the failure stand.
2564
+ - **Timeout is per-attempt.** Each attempt gets the step's full `timeout` budget — a timed-out attempt counts as one failed attempt and is retried while attempts remain. The total wall-clock can therefore approach `maxAttempts * (timeout + delay)`, so keep `maxAttempts` and `maxDelayMs` sane (the job-level `timeout` still bounds the whole job).
2565
+ - **Retries exhaust before `continueOnError`.** A step with both retries first; only the _final_ failure is then softened to a warning by `continueOnError`.
2566
+
2567
+ `retry` works identically under `kici run local` and on a remote agent, and applies to dynamically-generated job steps too. The `retryIf` predicate is an in-memory function: it is honored at execution time but never serialized into the lock file.
2568
+
2569
+ > **Retry vs. wait-until-condition.** `retry` re-runs a step that _throws_. To poll until a condition becomes true (a port listening, a `/health` endpoint returning 200, a unit becoming active), use [`waitForStep`](https://docs.kici.dev/user/sdk/wait-for/) instead — it is purpose-built for declarative wait-for-condition with intervals, a timeout, and on-timeout handling.
2570
+
2507
2571
  ### Output chaining
2508
2572
 
2509
2573
  Steps and jobs can access outputs from preceding steps/jobs using two patterns.
@@ -2586,7 +2650,7 @@ const deploy = job('deploy', {
2586
2650
 
2587
2651
  **Important:** `needs` must be declared explicitly. Output chaining does not auto-infer dependencies -- you must list job dependencies in `needs` even if you access their outputs via `.result`.
2588
2652
 
2589
- Cross-job output chaining works in both local test mode (`kici test`) and remote pipeline execution. The orchestrator's needs-aware dispatch scheduler guarantees upstream jobs reach a terminal state before downstream jobs dispatch, and upstream outputs are transported to the downstream agent sandbox via the `upstreamJobOutputs` field on `job.dispatch`. See [needs-scheduler](../../architecture/execution/needs-scheduler.md) for the full dispatch semantics.
2653
+ Cross-job output chaining works in both local execution (`kici run local`) and remote pipeline execution. The orchestrator's needs-aware dispatch scheduler guarantees upstream jobs reach a terminal state before downstream jobs dispatch, and upstream outputs are transported to the downstream agent sandbox via the `upstreamJobOutputs` field on `job.dispatch`. See [needs-scheduler](https://docs.kici.dev/architecture/execution/needs-scheduler/) for the full dispatch semantics.
2590
2654
 
2591
2655
  ### Job dependencies (`needs`)
2592
2656
 
@@ -2641,7 +2705,7 @@ job('report', {
2641
2705
  });
2642
2706
  ```
2643
2707
 
2644
- For an arbitrary outcome-based gate that prevents a job from dispatching at all, use a result-aware `dynamicJob` that returns `[]` or `[job]` based on `ctx.needs.<job>.status` — see [Dynamic jobs](../../architecture/execution/dynamic-jobs.md).
2708
+ For an arbitrary outcome-based gate that prevents a job from dispatching at all, use a result-aware `dynamicJob` that returns `[]` or `[job]` based on `ctx.needs.<job>.status` — see [Dynamic jobs](https://docs.kici.dev/architecture/execution/dynamic-jobs/).
2645
2709
 
2646
2710
  **DAG validation:** three-layer cycle detection.
2647
2711
 
@@ -2686,7 +2750,7 @@ Tag a dynamic job generator function with a group name so other jobs can referen
2686
2750
  function dynamicJob(groupName: string, fn: DynamicJobFn): DynamicJobFn;
2687
2751
  ```
2688
2752
 
2689
- The generator runs twice: once in the init phase (to register expected job names) and once inside the executing agent (to produce the actual jobs). Mismatches between the two evaluations are detected as determinism drift -- see [dynamic-jobs](../../architecture/execution/dynamic-jobs.md).
2753
+ The generator runs twice: once in the init phase (to register expected job names) and once inside the executing agent (to produce the actual jobs). Mismatches between the two evaluations are detected as determinism drift -- see [dynamic-jobs](https://docs.kici.dev/architecture/execution/dynamic-jobs/).
2690
2754
 
2691
2755
  ### Auto-generated IDs
2692
2756
 
@@ -3051,13 +3115,13 @@ The typed `payload` shapes above reference these partial GitHub object types. Ea
3051
3115
 
3052
3116
  Source: https://docs.kici.dev/user/sdk/idempotent/
3053
3117
 
3054
- The SDK exposes two idempotency helpers — a generic function `idempotent()` and a step factory `idempotentStep()` — for the common case where a workflow step should:
3118
+ The SDK exposes three idempotency helpers — a generic function `idempotent()`, the step factory `idempotentStep()`, and its check-mode-aware sibling `checkStep()` — for the common case where a workflow step should:
3055
3119
 
3056
3120
  1. **Check** whether the desired state is already in place.
3057
3121
  2. **Apply** the change only when drift is detected.
3058
3122
  3. **Surface** the resource (or its identifier) on both branches, so downstream steps don't need to know whether work happened or was skipped.
3059
3123
 
3060
- Both helpers wrap the same underlying runner, so they share semantics and return shape. Pick `idempotentStep()` when the operation is the whole job of a step; use `idempotent()` from anywhere — inside a multi-action step, a hook, or a bare async function.
3124
+ `idempotent()` and `idempotentStep()` wrap the same underlying runner and always apply on drift. Pick `idempotentStep()` when the operation is the whole job of a step; use `idempotent()` from anywhere — inside a multi-action step, a hook, or a bare async function. Pick `checkStep()` when the step should respect the run-level check mode — `kici run --check` previews the drift without applying it.
3061
3125
 
3062
3126
  ## `idempotent(options)`
3063
3127
 
@@ -3150,6 +3214,61 @@ export const setup = job('setup', {
3150
3214
  });
3151
3215
  ```
3152
3216
 
3217
+ ## `checkStep(name, options)`
3218
+
3219
+ The check-mode-aware sibling of `idempotentStep()`. It takes the **same option shape**, but behaves differently when a run is started in check mode (`kici run --check`):
3220
+
3221
+ | Factory | Behavior under `kici run --check` |
3222
+ | ---------------- | ----------------------------------------- |
3223
+ | `idempotentStep` | always applies on drift |
3224
+ | `checkStep` | reports drift, applies only in apply mode |
3225
+
3226
+ Use `checkStep()` for deploy-style steps where you want a dry-run preview of pending changes before committing them, and `idempotentStep()` for steps that must always converge (for example inside a hook). A `checkStep()` desugars to the first-class step check facet (`check` / `summarize` / `run(ctx, drift)` / `whenInSync`), so it participates in run-level check mode automatically: `kici run --check` reports the drift and skips `apply`, `kici run --check --fail-on-drift` exits non-zero when drift is detected, and apply mode applies the change.
3227
+
3228
+ ### Parameters
3229
+
3230
+ | Name | Type | Required | Description |
3231
+ | ----------------- | ------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
3232
+ | `name` | `string` | Yes | Step name. Appears in the run timeline and in log lines. |
3233
+ | `check` | `(ctx) => Promise<TDrift \| null>` | Yes | Read-only inspection. Return `null` when the system is already in the desired state. |
3234
+ | `apply` | `(ctx, drift: TDrift) => Promise<TApplied>` | Yes | Brings the system to the desired state. Runs only in apply mode (skipped under `kici run --check`). |
3235
+ | `summarize` | `(drift: TDrift) => string` | Yes | Human-readable summary of what `apply()` would do; shown in check-mode drift output. |
3236
+ | `whenInSync` | `(ctx) => Promise<TInSync>` | No | Runs when `check()` returned `null` (already in sync). |
3237
+ | `continueOnError` | `boolean` | No | When true, the job proceeds even if this step fails. |
3238
+ | `timeout` | `number` | No | Step-level timeout in milliseconds. |
3239
+
3240
+ The one signature difference from `idempotentStep`: `apply` and `whenInSync` receive `ctx` as their first argument, so the apply logic has access to `ctx.$`, `ctx.log`, and `ctx.secrets`.
3241
+
3242
+ ### Result
3243
+
3244
+ `checkStep(...)` returns `Step<TApplied | TInSync>` — the output is whichever of `apply` / `whenInSync` ran.
3245
+
3246
+ ### Example
3247
+
3248
+ ```typescript
3249
+ import { checkStep, job } from '@kici-dev/sdk';
3250
+
3251
+ const ensureDnsRecord = checkStep('ensure-dns-record', {
3252
+ check: async (ctx) => {
3253
+ const existing = await ctx.$`dig +short api.example.com`;
3254
+ return existing.stdout.trim() ? null : { fqdn: 'api.example.com', target: '203.0.113.10' };
3255
+ },
3256
+ summarize: (drift) => `Create A record ${drift.fqdn} → ${drift.target}`,
3257
+ apply: async (ctx, drift) => {
3258
+ await ctx.$`dns-cli create ${drift.fqdn} ${drift.target}`;
3259
+ return { created: true };
3260
+ },
3261
+ whenInSync: async () => ({ created: false }),
3262
+ });
3263
+
3264
+ export const deploy = job('deploy', {
3265
+ runsOn: 'linux',
3266
+ steps: [ensureDnsRecord],
3267
+ });
3268
+ ```
3269
+
3270
+ Run `kici run --check` against this workflow to see the drift summary without touching DNS; run it without `--check` to apply.
3271
+
3153
3272
  ## Worked example: create-if-missing returning a resource id
3154
3273
 
3155
3274
  The typical use case is **resource provisioning that should be safe to re-run**. The helper guarantees the same downstream typed shape whether the resource already existed or was just created:
@@ -3192,8 +3311,90 @@ The caller never has to branch on outcome — `result.result` is always a `Bucke
3192
3311
 
3193
3312
  ## See also
3194
3313
 
3195
- - [Core SDK reference](./core.md) — the `step()`, `job()`, and `workflow()` factories that `idempotentStep()` builds on.
3196
- - [Runtime types](./runtime.md) — `StepContext`, `Logger`, and other surface used inside the helpers.
3314
+ - [Core SDK reference](https://docs.kici.dev/user/sdk/core/) — the `step()`, `job()`, and `workflow()` factories that `idempotentStep()` builds on.
3315
+ - [Runtime types](https://docs.kici.dev/user/sdk/runtime/) — `StepContext`, `Logger`, and other surface used inside the helpers.
3316
+
3317
+ ---
3318
+
3319
+ ## SDK reference: parallel
3320
+
3321
+ Source: https://docs.kici.dev/user/sdk/parallel/
3322
+
3323
+ `parallel([...steps], opts?)` runs a group of independent steps **concurrently**
3324
+ within one job, behind a join barrier: execution continues past the group only
3325
+ once every child has settled. Each child is its own observable step — it gets its
3326
+ own logs, status, timing, and retry — instead of being hidden inside one step's
3327
+ `Promise.all`.
3328
+
3329
+ ```ts
3330
+ import { workflow, job, step, parallel, push } from '@kici-dev/sdk';
3331
+
3332
+ export default workflow('ci', {
3333
+ on: push(),
3334
+ jobs: [
3335
+ job('checks', {
3336
+ runsOn: 'kici:os:linux',
3337
+ steps: [
3338
+ checkout,
3339
+ // lint, typecheck, and the unit tests have no ordering between them,
3340
+ // so they run together — the job's wall-clock is the slowest child,
3341
+ // not the sum of all three.
3342
+ parallel([lint, typecheck, unitTests], { failFast: true }),
3343
+ deploy,
3344
+ ],
3345
+ }),
3346
+ ],
3347
+ });
3348
+ ```
3349
+
3350
+ `parallel(...)` returns a `ParallelGroup` that sits in the ordinary flat
3351
+ `steps: [...]` array — there is no new `job` field. A group's children are
3352
+ **sequential steps only**; groups cannot be nested.
3353
+
3354
+ ## Options
3355
+
3356
+ `parallel(steps, opts?)` accepts:
3357
+
3358
+ - **`failFast?: boolean`** — default `true`. When a child fails, the in-flight
3359
+ siblings are cancelled immediately and the job fails. With `failFast: false`
3360
+ every child runs to completion first, then the job fails if any child failed.
3361
+ - **`maxParallel?: number`** — default unlimited. Caps how many children run at
3362
+ once; children waiting for a slot report a `pending` status until they launch.
3363
+ - **`name?: string`** — a label for the group's dashboard band.
3364
+
3365
+ A child marked `continueOnError: true` never trips fail-fast and never fails the
3366
+ job — it still shows a `failed` status badge, but the group treats it as
3367
+ non-fatal.
3368
+
3369
+ ## Statuses
3370
+
3371
+ Parallel steps introduce two step statuses:
3372
+
3373
+ - **`pending`** — a child queued behind `maxParallel`, not yet launched.
3374
+ - **`cancelled`** — a sibling aborted by fail-fast. A cancelled step is **not** a
3375
+ failure: only the child that actually failed fails the job; the cancelled
3376
+ siblings render in gray (distinct from the red failing step) on the dashboard.
3377
+
3378
+ Children may also complete **out of order** — the fastest child finishes first
3379
+ regardless of array position. A later sequential step can read a parallel child's
3380
+ `.result` after the barrier; children within a group cannot read each other's
3381
+ results (there is no ordering inside the group).
3382
+
3383
+ ## Scope: nests inside job-level fan-out
3384
+
3385
+ `parallel()`'s `failFast` / `maxParallel` are **step-group** scopes — they govern
3386
+ only the steps inside the group. They are a different layer from the **job-level**
3387
+ `failFast` / `maxParallel` on a matrix / `runsOnAll` fan-out, which govern how a
3388
+ job's child _jobs_ spread across the matrix or host roster. A `parallel()` group
3389
+ inside a fan-out job nests its concurrency inside each fan-out child.
3390
+
3391
+ ## Local vs remote execution
3392
+
3393
+ Run remotely (the orchestrator + agent), parallel children execute concurrently
3394
+ and each surfaces as its own dashboard step. `kici run local` executes the same
3395
+ children in array order in its single-process model — the results are identical,
3396
+ only the wall-clock and the live fail-fast cancellation differ. Use a remote run
3397
+ to observe the concurrent timeline.
3197
3398
 
3198
3399
  ---
3199
3400
 
@@ -3320,7 +3521,7 @@ You can also narrow directly with `if (ctx.event.type === 'pull_request')` — T
3320
3521
 
3321
3522
  `EventPayload` is a discriminated union over the `type` field. Each variant provides typed access to the normalized event fields and the raw webhook payload.
3322
3523
 
3323
- Every variant carries the shared `EventBase` fields — `type`, `action`, `targetBranch`, `sourceBranch`, `provider`, `isForkPR`, `baseBranch`, `senderUsername`, `sourceRepo`, `changedFiles`, and the raw `payload` — plus a per-type `payload` shape for the typed variants. The complete field-by-field schema, including every typed `payload` shape and the shared GitHub object types, is in the [event payload reference](./event-payloads.md).
3524
+ Every variant carries the shared `EventBase` fields — `type`, `action`, `targetBranch`, `sourceBranch`, `provider`, `isForkPR`, `baseBranch`, `senderUsername`, `sourceRepo`, `changedFiles`, and the raw `payload` — plus a per-type `payload` shape for the typed variants. The complete field-by-field schema, including every typed `payload` shape and the shared GitHub object types, is in the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/).
3324
3525
 
3325
3526
  **Typed variants** (with GitHub-specific payload fields): `pull_request`, `push`, `tag`, `comment`, `review`, `review_comment`, `release`, `dispatch`, `create`, `delete`, `status`, `workflow_run`, `fork`, `star`, `watch`.
3326
3527
 
@@ -3430,7 +3631,7 @@ interface MatrixValues {
3430
3631
 
3431
3632
  A matrix fan-out runs every combination at once by default. The fan-out-generic
3432
3633
  `maxParallel` and `failFast` job options bound it the same way they bound a
3433
- [`runsOnAll`](./runs-on-all.md#rolling-rollout-maxparallel--failfast) host fan-out:
3634
+ [`runsOnAll`](https://docs.kici.dev/user/sdk/runs-on-all/#rolling-rollout-maxparallel--failfast) host fan-out:
3434
3635
 
3435
3636
  ```typescript
3436
3637
  const test = job('test', {
@@ -3583,7 +3784,7 @@ export default workflow('discovery-fan-out', { jobs: [discover, reports] });
3583
3784
  | `'jobName'` / `{ name, when }` | `{ result, status }` — `result` is an `OutputProxy` (`ctx.needs.<job>.result.<step>.<field>`; single-step `run` jobs flatten to `ctx.needs.<job>.result.<field>`); `status` is the upstream's terminal status |
3584
3785
  | `dynamicGroup('g')` / `dynamicGroup('g', { when })` | ordered array of `{ name, result, status }`, one per group member |
3585
3786
 
3586
- `ctx.needs` is deterministic — a snapshot of upstream outputs frozen at first eval and replayed unchanged on re-eval, like `ctx.event`. Use result-aware generation for same-run fan-out from a prior job's result; use [`jobComplete()`](./triggers.md) for cross-workflow reactions to a job finishing. See the architecture deep-dive in [dynamic jobs](../../architecture/execution/dynamic-jobs.md#result-aware-generation).
3787
+ `ctx.needs` is deterministic — a snapshot of upstream outputs frozen at first eval and replayed unchanged on re-eval, like `ctx.event`. Use result-aware generation for same-run fan-out from a prior job's result; use [`jobComplete()`](https://docs.kici.dev/user/sdk/triggers/) for cross-workflow reactions to a job finishing. See the architecture deep-dive in [dynamic jobs](https://docs.kici.dev/architecture/execution/dynamic-jobs/#result-aware-generation).
3587
3788
 
3588
3789
  ### JobOrFactory
3589
3790
 
@@ -3624,7 +3825,7 @@ roster that matches a label predicate — one pinned execution per host. Use it
3624
3825
  fleet-wide operations: patch every web tier, smoke-test every node, collect uptime
3625
3826
  from the fleet.
3626
3827
 
3627
- `runsOnAll` is mutually exclusive with [`runsOn`](/user/sdk/core/): a job declares one
3828
+ `runsOnAll` is mutually exclusive with [`runsOn`](https://docs.kici.dev/user/sdk/core/): a job declares one
3628
3829
  or the other. Where `runsOn` picks a **single** agent that satisfies the labels,
3629
3830
  `runsOnAll` targets **all** matching hosts and runs the job once on each, pinned to
3630
3831
  that specific host.
@@ -3673,7 +3874,7 @@ const patch = job('patch', {
3673
3874
  #### Targeting by pattern
3674
3875
 
3675
3876
  Every entry in any of these forms — include or exclude — can be an exact string, a
3676
- glob, or a regular expression, exactly like [`runsOn`](./core.md#targeting-by-pattern):
3877
+ glob, or a regular expression, exactly like [`runsOn`](https://docs.kici.dev/user/sdk/core/#targeting-by-pattern):
3677
3878
 
3678
3879
  - **Plain string → exact match** (`'role:web'`).
3679
3880
  - **String with glob metacharacters (`*`, `?`, `[]`, `{}`) → glob** (`'kici:host:web-*'`).
@@ -3725,6 +3926,66 @@ run: async (ctx) => {
3725
3926
 
3726
3927
  Both are `undefined` for jobs that do not use `runsOnAll`.
3727
3928
 
3929
+ ### ctx.fanout — fan-out position
3930
+
3931
+ Every fan-out child — a `runsOnAll` host **or** a matrix combination — also
3932
+ carries its **position** within the fan-out:
3933
+
3934
+ ```typescript
3935
+ ctx.fanout?: {
3936
+ index: number; // 0-based position in the deterministically-ordered fan-out
3937
+ total: number; // number of children in this fan-out
3938
+ first: boolean; // index === 0
3939
+ last: boolean; // index === total - 1
3940
+ };
3941
+ ```
3942
+
3943
+ The order is a **documented guarantee**: host fan-out is ordered by agent id,
3944
+ matrix fan-out by its combination label. So `ctx.fanout.first` is always the
3945
+ same (lowest-agent-id) host across re-runs, and `ctx.fanout.last` the same final
3946
+ one. `ctx.fanout` is `undefined` on a job that is not fanned out.
3947
+
3948
+ ### Run-once steps: onlyOnFirstHost / onlyOnLastHost / onlyOnFanoutIndex
3949
+
3950
+ For ordered, stateful rollouts you often need a step that runs on exactly **one**
3951
+ host — enable a leader before the rest join, run a one-time migration, take a
3952
+ single dump. Three rule helpers express this by reading `ctx.fanout`:
3953
+
3954
+ ```typescript
3955
+ import { job, step, onlyOnFirstHost, onlyOnLastHost, onlyOnFanoutIndex } from '@kici-dev/sdk';
3956
+
3957
+ const rollout = job('rollout', {
3958
+ runsOnAll: 'role:db',
3959
+ maxParallel: 1, // serial, so "first" runs before the rest
3960
+ steps: [
3961
+ // Runs only on the first (lowest-agent-id) host — KiCI's run-once primitive.
3962
+ step('enable-sync-mode', { rules: [onlyOnFirstHost()] }, async (ctx) => {
3963
+ /* configure the leader before standbys join */
3964
+ }),
3965
+ // Runs on every host.
3966
+ step('apply', async (ctx) => {
3967
+ /* ... */
3968
+ }),
3969
+ // Runs only on the last host.
3970
+ step('finalize', { rules: [onlyOnLastHost()] }, async (ctx) => {
3971
+ /* ... */
3972
+ }),
3973
+ ],
3974
+ });
3975
+ ```
3976
+
3977
+ - A step gated this way is **skipped** (not failed) on non-matching hosts — its
3978
+ outputs exist only on the host where it ran.
3979
+ - `onlyOnFanoutIndex(n)` targets the host at a specific position.
3980
+ - **Non-fan-out safety:** on a job that is not fanned out, `ctx.fanout` is
3981
+ `undefined` and these helpers treat the job as a single implicit child at
3982
+ index 0 — so `onlyOnFirstHost()` runs normally there (there is one host, which
3983
+ is the first). This means you can author a step with `onlyOnFirstHost()` and it
3984
+ behaves correctly whether or not the job ends up fanning out.
3985
+ - The helpers are host-flavored by name (the dominant use case) but read
3986
+ `ctx.fanout`, so they work for matrix fan-out too — `onlyOnFirstHost()` runs on
3987
+ the first combination.
3988
+
3728
3989
  ### byHost outputs
3729
3990
 
3730
3991
  A downstream that `needs:` a `runsOnAll` job receives a **byHost** envelope instead
@@ -3757,7 +4018,7 @@ to a single scalar: `summary.outputs[key]` is an array of every host's value, an
3757
4018
  ### onUnreachable: skip | fail | hold
3758
4019
 
3759
4020
  Resolution is backed by the **declared host roster** (see the operator
3760
- [host roster](/operator/orchestrator/host-roster/) doc), not just the live registry.
4021
+ [host roster](https://docs.kici.dev/operator/orchestrator/host-roster/) doc), not just the live registry.
3761
4022
  This lets KiCI surface an expected-but-absent host instead of silently fanning out to a
3762
4023
  partial fleet. The `onUnreachable` policy controls what happens when a **durable**
3763
4024
  (static) host in the roster is matched but not currently connected:
@@ -3783,6 +4044,37 @@ independent of `onUnreachable` — a scaled-down node may never return. A `runsO
3783
4044
  that matches zero usable hosts fails the run rather than reporting a silent zero-child
3784
4045
  success.
3785
4046
 
4047
+ ### includeUninitialized: converge a fresh fleet
4048
+
4049
+ `onUnreachable` governs declared hosts that _had_ an agent and are momentarily absent.
4050
+ A **never-initialized** host — a freshly-provisioned box reachable over SSH but with no
4051
+ agent yet — is a different case: there is nothing to run on. Set
4052
+ `includeUninitialized: true` to widen the fan-out to those hosts and bring them up:
4053
+
4054
+ ```typescript
4055
+ const converge = job('converge', {
4056
+ runsOnAll: 'kici:group:prod',
4057
+ includeUninitialized: true,
4058
+ steps: [partitionDisk, formatLuks, debootstrap, installAgent],
4059
+ });
4060
+ ```
4061
+
4062
+ For each un-agented declared host (one carrying SSH reach metadata), KiCI brings up a
4063
+ temporary init-runner over SSH and runs the **same steps** on it; hosts that already
4064
+ have a live agent run the steps on their own agent. One workflow converges the whole
4065
+ fleet — fresh boxes get built, live boxes run the same phases.
4066
+
4067
+ Because the steps run on already-initialized hosts too, the bootstrap phases **must be
4068
+ idempotent [check-steps](https://docs.kici.dev/user/sdk/core/)**: each step's `check()` reports in-sync on a
4069
+ live box so the partition / format / install steps **skip** there and run only on fresh
4070
+ boxes. This is the safety guard — an OS or disk-format step must never re-run on a host
4071
+ that is already built. Re-running the workflow is a no-op everywhere. See the operator
4072
+ [fresh-box bootstrap](https://docs.kici.dev/operator/orchestrator/host-roster/) doc for the bring-up,
4073
+ capability gating, and lifecycle details.
4074
+
4075
+ `includeUninitialized` is only meaningful alongside `runsOnAll`; it is ignored on a
4076
+ single-agent `runsOn` job.
4077
+
3786
4078
  ### Rolling rollout: maxParallel + failFast
3787
4079
 
3788
4080
  By default a `runsOnAll` fan-out dispatches to every matched host at once — fine for
@@ -3840,8 +4132,8 @@ When `--target` narrows a `runsOnAll` job to zero hosts, the run **fails** by de
3840
4132
  (a mistyped selector should be loud, not silently no-op). Pass `--target-allow-empty`
3841
4133
  to **skip** the zeroed job instead — it records a `skipped` status, and downstream jobs
3842
4134
  gated with `when: 'on-skip'` (or `when: 'always'`) still run, exactly as for an
3843
- `onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](/user/cli-reference/#host-narrowing-with---target)
3844
- for the full flag behavior and the [`needs` gating model](./core.md#job-dependencies-needs)
4135
+ `onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](https://docs.kici.dev/user/cli-reference/#host-narrowing-with---target)
4136
+ for the full flag behavior and the [`needs` gating model](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs)
3845
4137
  for how a skipped upstream propagates.
3846
4138
 
3847
4139
  ### Limits (v0)
@@ -3875,9 +4167,9 @@ All types are exported from `@kici-dev/sdk` as type-only imports.
3875
4167
  | `OutputSchema` | Record of Zod types for step outputs |
3876
4168
  | `InferOutputs<T>` | Infer output type from output schema |
3877
4169
  | `ContainerConfig` | Container config for job execution (`image`, `env?`) |
3878
- | `RunsOn` | Union of `runsOn` forms: `string \| RegExp \| (string \| RegExp)[] \| RunsOnSelector`. A plain string matches exactly, a string with glob metacharacters (`*?[]{}`) is a glob, and a `RegExp` is a regular expression. See [Targeting by pattern](./core.md#targeting-by-pattern). |
4170
+ | `RunsOn` | Union of `runsOn` forms: `string \| RegExp \| (string \| RegExp)[] \| RunsOnSelector`. A plain string matches exactly, a string with glob metacharacters (`*?[]{}`) is a glob, and a `RegExp` is a regular expression. See [Targeting by pattern](https://docs.kici.dev/user/sdk/core/#targeting-by-pattern). |
3879
4171
  | `RunsOnSelector` | Object form for `runsOn` with `labels` (required) and `exclude` (optional) properties. Each element accepts the exact / glob / regex forms on both sides. |
3880
- | `RunsOnAllInput` | Union of `runsOnAll` host fan-out forms: `string \| RegExp \| (string \| RegExp)[] \| { include: { all: (string \| RegExp)[] }[]; exclude?: (string \| RegExp)[] }`. Same exact / glob / regex semantics per element. See [runsOnAll](./runs-on-all.md#targeting-by-pattern). |
4172
+ | `RunsOnAllInput` | Union of `runsOnAll` host fan-out forms: `string \| RegExp \| (string \| RegExp)[] \| { include: { all: (string \| RegExp)[] }[]; exclude?: (string \| RegExp)[] }`. Same exact / glob / regex semantics per element. See [runsOnAll](https://docs.kici.dev/user/sdk/runs-on-all/#targeting-by-pattern). |
3881
4173
  | `Fixture` | Test fixture definition returned by `fixture()` |
3882
4174
  | `FixtureOptions` | Options for `fixture()` factory |
3883
4175
  | `Registry` | Private npm registry declaration used in `WorkflowOptions.registries` |
@@ -4004,7 +4296,7 @@ interface StepContext<TInputs = Record<string, unknown>> {
4004
4296
  rawPayload?: Record<string, unknown>;
4005
4297
  /** Which git provider triggered this workflow (e.g. 'github', 'gitlab') */
4006
4298
  provider?: string;
4007
- /** Whether this execution was triggered by `kici test` (remote test run) */
4299
+ /** Whether this execution was triggered by `kici run remote` (developer-initiated remote run) */
4008
4300
  isTestRun: boolean;
4009
4301
  /** The resolved deployment environment name for this job (undefined without environment) */
4010
4302
  environment?: string;
@@ -4064,7 +4356,7 @@ step('example', async ({ $, log, env, matrix, workflow, job }) => {
4064
4356
  - **Inside a dynamic `environment` / `env` / `concurrencyGroup` function** on a static job — captured to the `__init__` job's synthetic step-0 log, which appears in the timeline as "Init: _jobname_".
4065
4357
  - **Inside a `DynamicJobFn` body and the per-generated-job `environment` / `env` / `concurrencyGroup` / `matrix` functions** — captured to the `__dynamic__` job's synthetic step-0 log ("Evaluate: _jobname_" in the timeline). The `$` parameter in that context is a scoped zx shell, so `await $\`...\`` subprocess output is captured too.
4066
4358
 
4067
- Use whichever style is convenient — you don't have to wrap `console.log` in the provided `log` parameter to make it visible. One limitation applies to in-process contexts only (init, build, dynamic-eval): direct `process.stdout.write` / `printf` is not captured there, because the agent's own logger uses that path and we don't want agent-internal output leaking into your step logs. Use `console.*` or the `log` parameter instead. See [Log streaming](../../architecture/execution/job-execution.md#log-streaming) for the full capture surface and limits (default 10 MB per step, backpressure behavior).
4359
+ Use whichever style is convenient — you don't have to wrap `console.log` in the provided `log` parameter to make it visible. One limitation applies to in-process contexts only (init, build, dynamic-eval): direct `process.stdout.write` / `printf` is not captured there, because the agent's own logger uses that path and we don't want agent-internal output leaking into your step logs. Use `console.*` or the `log` parameter instead. See [Log streaming](https://docs.kici.dev/architecture/execution/job-execution/#log-streaming) for the full capture surface and limits (default 10 MB per step, backpressure behavior).
4068
4360
 
4069
4361
  ### setEnv(key, value)
4070
4362
 
@@ -4089,7 +4381,7 @@ step('use', async (ctx) => {
4089
4381
  - Last-write-wins -- if multiple steps set the same key, the last value is used
4090
4382
  - Cannot override operator-injected secrets (the operator value takes precedence)
4091
4383
  - Changes take effect immediately in the current step and persist for all subsequent steps
4092
- - Shell commands export the same way by appending to `$KICI_ENV` (see [Exporting env from shell commands](#exporting-env-from-shell-commands-kici_env--kici_path) below)
4384
+ - Shell commands export the same way by appending to `$KICI_ENV` (see [Exporting env from shell commands](https://docs.kici.dev/user/sdk/runtime/#exporting-env-from-shell-commands-kici_env--kici_path) below)
4093
4385
 
4094
4386
  ### addPath(dir)
4095
4387
 
@@ -4302,7 +4594,7 @@ const publish = job('publish', {
4302
4594
  - The returned `{ storageKey, subjectDigest, bundleMediaType }` identifies the stored bundle.
4303
4595
  - Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.
4304
4596
 
4305
- See the [build provenance guide](../provenance.md) for the end-to-end attest →
4597
+ See the [build provenance guide](https://docs.kici.dev/user/provenance/) for the end-to-end attest →
4306
4598
  verify → view journey, including how to verify a bundle with `kici verify-attestation`.
4307
4599
 
4308
4600
  ## Secrets
@@ -4397,11 +4689,11 @@ step('discover', async (ctx) => {
4397
4689
 
4398
4690
  ### File-mounted secrets (ctx.secrets.mountFile / exposeFile)
4399
4691
 
4400
- Tools that require a file path on disk (sops `SOPS_AGE_KEY_FILE`, kubectl `KUBECONFIG`, gcloud `GOOGLE_APPLICATION_CREDENTIALS`) get a typed step-side API: `ctx.secrets.mountFile(opts)` writes the concatenation of one or more existing secrets to a per-step tmpfile and returns the path; `ctx.secrets.exposeFile(envVar, opts)` additionally sets `process.env[envVar] = path`. Files are removed and env vars are unset automatically when the step completes (success, failure, or timeout) — no manual cleanup. See [Mounting secrets as files](../secrets.md#mounting-secrets-as-files) for the full options table, lifecycle details, and the canonical sops example.
4692
+ Tools that require a file path on disk (sops `SOPS_AGE_KEY_FILE`, kubectl `KUBECONFIG`, gcloud `GOOGLE_APPLICATION_CREDENTIALS`) get a typed step-side API: `ctx.secrets.mountFile(opts)` writes the concatenation of one or more existing secrets to a per-step tmpfile and returns the path; `ctx.secrets.exposeFile(envVar, opts)` additionally sets `process.env[envVar] = path`. Files are removed and env vars are unset automatically when the step completes (success, failure, or timeout) — no manual cleanup. See [Mounting secrets as files](https://docs.kici.dev/user/secrets/#mounting-secrets-as-files) for the full options table, lifecycle details, and the canonical sops example.
4401
4693
 
4402
4694
  ### Local test mode secrets
4403
4695
 
4404
- When running `kici test`, you can provide secrets locally without an orchestrator.
4696
+ When running `kici preview`, you can provide secrets locally without an orchestrator.
4405
4697
 
4406
4698
  #### .kici/.secrets file
4407
4699
 
@@ -4429,10 +4721,10 @@ Override or supplement file-based secrets with CLI flags:
4429
4721
 
4430
4722
  ```bash
4431
4723
  # Inject flat secrets (repeatable)
4432
- kici test push --secret DEPLOY_TOKEN=my-token --secret API_KEY=my-key
4724
+ kici preview push --secret DEPLOY_TOKEN=my-token --secret API_KEY=my-key
4433
4725
 
4434
4726
  # Inject context-scoped secrets (repeatable)
4435
- kici test push --context production.DB_PASSWORD=prod-secret --context npm-publish.NPM_TOKEN=abc123
4727
+ kici preview push --context production.DB_PASSWORD=prod-secret --context npm-publish.NPM_TOKEN=abc123
4436
4728
  ```
4437
4729
 
4438
4730
  **Precedence:** CLI flags override `.kici/.secrets` file values. Context secrets are auto-flattened into `ctx.secrets` using the same merge logic as production (last context wins).
@@ -4650,13 +4942,73 @@ Create a repository_dispatch trigger. Returns a frozen `DispatchTriggerConfig`.
4650
4942
  function dispatch(config?: DispatchConfigInput): DispatchTriggerConfig;
4651
4943
  ```
4652
4944
 
4653
- **Config options:** `types` (string[]), `description`
4945
+ **Config options:** `types` (string[]), `description`, `inputs` (typed dispatch inputs map)
4654
4946
 
4655
4947
  ```typescript
4656
4948
  dispatch(); // Any dispatch
4657
4949
  dispatch({ types: ['deploy', 'rollback'] }); // Specific event types
4658
4950
  ```
4659
4951
 
4952
+ #### Typed dispatch inputs
4953
+
4954
+ A `dispatch()` trigger can declare a typed `inputs` schema. Operators supply
4955
+ values with `kici run --input key=value`; KiCI validates, coerces, defaults, and
4956
+ exposes them to steps and rules as `ctx.dispatchInputs`. The values are validated
4957
+ on the orchestrator from the compiled lock file — a missing required input or a
4958
+ bad value is rejected before any agent runs, without cloning the repository.
4959
+
4960
+ ```typescript
4961
+ import { workflow, job, step, dispatch, defineDispatchInputs, z } from '@kici-dev/sdk';
4962
+
4963
+ const inputs = defineDispatchInputs({
4964
+ target: z.string().optional(),
4965
+ skipCveScan: z.boolean().default(false),
4966
+ skipCveScanReason: z.string().min(1).optional(),
4967
+ mode: z.enum(['full', 'edge-only']).default('full'),
4968
+ retries: z.number().int().min(0).max(10).default(3),
4969
+ });
4970
+
4971
+ export default workflow('deploy-prod', {
4972
+ on: dispatch({ types: ['deploy-prod'], inputs }),
4973
+ jobs: [
4974
+ job('gates', {
4975
+ runsOn: 'kici:group:ops',
4976
+ steps: [
4977
+ step('cve-gate', async (ctx) => {
4978
+ const i = inputs.from(ctx); // fully typed per declared key
4979
+ if (i.skipCveScan) {
4980
+ ctx.log.warn(`CVE gate skipped: ${i.skipCveScanReason ?? '(no reason)'}`);
4981
+ return;
4982
+ }
4983
+ await ctx.$`pnpm scan:cve:gate`;
4984
+ }),
4985
+ ],
4986
+ }),
4987
+ ],
4988
+ });
4989
+ ```
4990
+
4991
+ - **`defineDispatchInputs(map)`** is the single declaration site. It returns a
4992
+ handle that `dispatch({ inputs })` accepts and exposes `inputs.from(ctx)` — a
4993
+ typed reader over `ctx.dispatchInputs`, typed per declared key. `dispatch({ inputs })`
4994
+ also accepts a bare `{ name: schema }` map directly when you don't need the reader.
4995
+ - **`ctx.dispatchInputs`** is always present (a validated map of
4996
+ `string | number | boolean | null`), distinct from `ctx.inputs` (typed outputs
4997
+ from `needs` dependencies). Rules see the same values via `ctx.dispatchInputs`,
4998
+ so `skipUnless(ctx => !ctx.dispatchInputs.skipCveScan)` works.
4999
+ - **Defaults are applied once**, on the orchestrator (the authoritative side); the
5000
+ CLI pre-validates `--input` for fast feedback and forwards the raw operator pairs.
5001
+
5002
+ **Allowed input types (closed subset):** `z.string()`, `z.number()`,
5003
+ `z.boolean()`, `z.enum([...])`, `z.literal(v)`, with the modifiers `.optional()`,
5004
+ `.nullable()`, `.default(v)`, `.min(n)`, `.max(n)`, `.regex(re)`, `.int()`.
5005
+ Anything outside this set (`.refine()`, `.transform()`, `.pipe()`, `z.object()`,
5006
+ `z.array()`, `z.union()`, `z.record()`, `z.coerce.*`) is a **compile error** —
5007
+ the closed set is what guarantees the schema survives the trip to the
5008
+ orchestrator's lock file without silently dropping any validation. CLI strings
5009
+ are coerced for you (`--input retries=3` becomes the number `3`; booleans accept
5010
+ `true`/`false`/`1`/`0`/`yes`/`no`), so author your schema with clean types.
5011
+
4660
5012
  ### create()
4661
5013
 
4662
5014
  Create a ref creation trigger (branches/tags). Returns a frozen `CreateTriggerConfig`.
@@ -4770,7 +5122,7 @@ Create a catch-all webhook trigger for any GitHub event. Returns a frozen `Webho
4770
5122
  function webhook(config: WebhookConfigInput): WebhookTriggerConfig;
4771
5123
  ```
4772
5124
 
4773
- **Config options:** `events` (required string[]), `actions` (optional string[]), `repos` (optional cross-repo source patterns -- see [global workflows](../global-workflows.md)), `description`
5125
+ **Config options:** `events` (required string[]), `actions` (optional string[]), `repos` (optional cross-repo source patterns -- see [global workflows](https://docs.kici.dev/user/global-workflows/)), `description`
4774
5126
 
4775
5127
  ```typescript
4776
5128
  webhook({ events: ['deployment'] }); // Deployment events
@@ -4875,7 +5227,7 @@ jobComplete({ workflow: 'CI', job: 'build', status: ['success'] }); // Success o
4875
5227
  jobComplete({ workflow: 'CI', job: 'build', source: 'org/repo' }); // Cross-repo
4876
5228
  ```
4877
5229
 
4878
- `jobComplete()` starts a **new** workflow run that reacts to another job finishing (gated on the prior job's status). For same-run fan-out — generating follow-up jobs from a prior job's _outputs_ within the same run — use a result-aware [`dynamicJob(group, { needs, generate })`](./rules-matrix-dynamic.md#dynamicjob--result-aware-generation) instead.
5230
+ `jobComplete()` starts a **new** workflow run that reacts to another job finishing (gated on the prior job's status). For same-run fan-out — generating follow-up jobs from a prior job's _outputs_ within the same run — use a result-aware [`dynamicJob(group, { needs, generate })`](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/#dynamicjob--result-aware-generation) instead.
4879
5231
 
4880
5232
  ### genericWebhook()
4881
5233
 
@@ -4927,6 +5279,7 @@ interface ScheduleConfigInput {
4927
5279
  cron: string; // Required: cron expression (5-field)
4928
5280
  timezone?: string; // Timezone for cron evaluation (default: 'UTC')
4929
5281
  description?: string; // Human-readable description of the schedule
5282
+ inputs?: DispatchInputsMap; // Optional: defaults-only typed inputs (see below)
4930
5283
  }
4931
5284
  ```
4932
5285
 
@@ -4937,6 +5290,41 @@ schedule({ cron: '0 9 * * 1', timezone: 'America/New_York' }); // Monday 9am ET
4937
5290
  schedule({ cron: '*/15 * * * *', description: 'health check every 15 min' });
4938
5291
  ```
4939
5292
 
5293
+ #### Schedule inputs (defaults-only)
5294
+
5295
+ A `schedule()` trigger may declare typed `inputs`. A cron or dashboard
5296
+ "run now" fire carries **no operator-supplied values**, so each input resolves
5297
+ from its declared **default** and is exposed to steps and rules as
5298
+ `ctx.dispatchInputs` — the same surface as [typed dispatch inputs](https://docs.kici.dev/user/sdk/triggers/#typed-dispatch-inputs).
5299
+
5300
+ Because there is no operator to supply a value, every schedule input must
5301
+ declare a `.default()` **or** be `.optional()`. An input that is neither is
5302
+ rejected at `kici compile` time.
5303
+
5304
+ ```typescript
5305
+ import { workflow, job, schedule, z } from '@kici-dev/sdk';
5306
+
5307
+ export default workflow('nightly', {
5308
+ on: schedule({
5309
+ cron: '0 3 * * *',
5310
+ inputs: { mode: z.enum(['full', 'quick']).default('full') },
5311
+ }),
5312
+ jobs: [
5313
+ job('build', {
5314
+ runsOn: 'default',
5315
+ run: async (ctx) => {
5316
+ ctx.log(`mode = ${ctx.dispatchInputs.mode}`); // "full" on every fire
5317
+ },
5318
+ }),
5319
+ ],
5320
+ });
5321
+ ```
5322
+
5323
+ You can also share a typed handle via `defineDispatchInputs(...)` and read it
5324
+ back with `.from(ctx)`, exactly as with `dispatch()`. The allowed input types
5325
+ are the same closed subset documented under
5326
+ [typed dispatch inputs](https://docs.kici.dev/user/sdk/triggers/#typed-dispatch-inputs).
5327
+
4940
5328
  ### lifecycle()
4941
5329
 
4942
5330
  Create a lifecycle trigger for cross-workflow orchestration events. Returns a frozen `LifecycleTriggerConfig`.
@@ -5255,9 +5643,9 @@ If `check()` throws while polling, the error is logged and polling continues —
5255
5643
 
5256
5644
  ## See also
5257
5645
 
5258
- - [Core SDK reference](./core.md) — the `step()`, `job()`, and `workflow()` factories that `waitForStep()` builds on.
5259
- - [Idempotent helpers](./idempotent.md) — `idempotent()` and `idempotentStep()` for check / apply patterns.
5260
- - [Runtime types](./runtime.md) — `StepContext`, `Logger`, and other surface used inside the helpers.
5646
+ - [Core SDK reference](https://docs.kici.dev/user/sdk/core/) — the `step()`, `job()`, and `workflow()` factories that `waitForStep()` builds on.
5647
+ - [Idempotent helpers](https://docs.kici.dev/user/sdk/idempotent/) — `idempotent()` and `idempotentStep()` for check / apply patterns.
5648
+ - [Runtime types](https://docs.kici.dev/user/sdk/runtime/) — `StepContext`, `Logger`, and other surface used inside the helpers.
5261
5649
 
5262
5650
  ---
5263
5651
 
@@ -5265,18 +5653,19 @@ If `check()` throws while polling, the error is logged and polling continues —
5265
5653
 
5266
5654
  Source: https://docs.kici.dev/user/sdk-reference/
5267
5655
 
5268
- Reference documentation for `@kici-dev/sdk`. The reference is split across five pages by topic.
5656
+ Reference documentation for `@kici-dev/sdk`. The reference is split across the per-topic pages below.
5269
5657
 
5270
5658
  | Page | Covers |
5271
5659
  | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5272
- | [Core](./sdk/core.md) | `workflow()`, `job()`, `step()` factory functions and step / job authoring patterns (bare functions, output chaining, `needs`, dynamic groups). |
5273
- | [Triggers](./sdk/triggers.md) | All 22 trigger factories -- GitHub events (`pr`, `push`, `tag`, `comment`, ...), event triggers (`kiciEvent`, `workflowComplete`, `jobComplete`), `genericWebhook`, `schedule`, `lifecycle`, plus branch-pattern semantics. |
5274
- | [Rules, matrix, dynamic jobs](./sdk/rules-matrix-dynamic.md) | `rule()`, `skip()`, matrix builds (static + dynamic), and `dynamicJob()` / `dynamicGroup()`. |
5275
- | [Caching](./sdk/caching.md) | `CacheSpec`, declarative `cache` on jobs/steps, imperative `ctx.cache.restore()` / `ctx.cache.save()`, immutable keys, `restoreKeys` prefix fallback, per-org + per-ref isolation. |
5276
- | [Validation & events](./sdk/validation-events.md) | `validateDag()`, `defineEvent()`, event emission patterns. |
5277
- | [Runtime](./sdk/runtime.md) | Types index, `StepContext`, secrets, and fixtures. |
5278
- | [Idempotent helpers](./sdk/idempotent.md) | `idempotent()` and `idempotentStep()` — check / apply pattern with typed results on both the skipped and applied branches. |
5279
- | [Wait-for helpers](./sdk/wait-for.md) | `waitFor()` and `waitForStep()` — poll a condition on an interval, run an optional success action, recover gracefully on timeout. |
5660
+ | [Core](https://docs.kici.dev/user/sdk/core/) | `workflow()`, `job()`, `step()` factory functions and step / job authoring patterns (bare functions, output chaining, `needs`, dynamic groups). |
5661
+ | [Triggers](https://docs.kici.dev/user/sdk/triggers/) | All 22 trigger factories -- GitHub events (`pr`, `push`, `tag`, `comment`, ...), event triggers (`kiciEvent`, `workflowComplete`, `jobComplete`), `genericWebhook`, `schedule`, `lifecycle`, plus branch-pattern semantics. |
5662
+ | [Rules, matrix, dynamic jobs](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/) | `rule()`, `skip()`, matrix builds (static + dynamic), and `dynamicJob()` / `dynamicGroup()`. |
5663
+ | [Caching](https://docs.kici.dev/user/sdk/caching/) | `CacheSpec`, declarative `cache` on jobs/steps, imperative `ctx.cache.restore()` / `ctx.cache.save()`, immutable keys, `restoreKeys` prefix fallback, per-org + per-ref isolation. |
5664
+ | [Validation & events](https://docs.kici.dev/user/sdk/validation-events/) | `validateDag()`, `defineEvent()`, event emission patterns. |
5665
+ | [Runtime](https://docs.kici.dev/user/sdk/runtime/) | Types index, `StepContext`, secrets, and fixtures. |
5666
+ | [Idempotent helpers](https://docs.kici.dev/user/sdk/idempotent/) | `idempotent()`, `idempotentStep()`, and the check-mode-aware `checkStep()` — check / apply pattern with typed results on both the skipped and applied branches. |
5667
+ | [Wait-for helpers](https://docs.kici.dev/user/sdk/wait-for/) | `waitFor()` and `waitForStep()` — poll a condition on an interval, run an optional success action, recover gracefully on timeout. |
5668
+ | [Parallel steps](https://docs.kici.dev/user/sdk/parallel/) | `parallel()` — run independent steps concurrently within one job behind a join barrier, each as its own observable step, with `failFast` and `maxParallel` controls. |
5280
5669
 
5281
5670
  The `@kici-dev/sdk` package re-exports the entire surface from a single entry point. Pick what you need:
5282
5671
 
@@ -5288,17 +5677,110 @@ For the complete list of every named export (factory functions, triggers, rules,
5288
5677
 
5289
5678
  ## See also
5290
5679
 
5291
- - [Getting started](getting-started.md) -- install the SDK, write your first workflow, test locally
5292
- - [CLI reference](cli-reference.md) -- compile, test, and manage workflows from the command line
5293
- - [Workflow patterns](workflow-patterns.md) -- common patterns using the SDK features documented above
5294
- - [Secrets management (operator)](../operator/security/secrets.md) -- configure encrypted secret storage and admin API
5295
- - [Secrets architecture](../architecture/security/secrets.md) -- encryption model, multi-backend, and data flow
5296
- - [State machine](../architecture/execution/state-machine.md) -- how execution states map to the lifecycle of jobs and steps
5680
+ - [Getting started](https://docs.kici.dev/user/getting-started/) -- install the SDK, write your first workflow, test locally
5681
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/) -- compile, test, and manage workflows from the command line
5682
+ - [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/) -- common patterns using the SDK features documented above
5683
+ - [Secrets management (operator)](https://docs.kici.dev/operator/security/secrets/) -- configure encrypted secret storage and admin API
5684
+ - [Secrets architecture](https://docs.kici.dev/architecture/security/secrets/) -- encryption model, multi-backend, and data flow
5685
+ - [State machine](https://docs.kici.dev/architecture/execution/state-machine/) -- how execution states map to the lifecycle of jobs and steps
5297
5686
 
5298
5687
  ---
5299
5688
 
5300
5689
  # CLI and authoring
5301
5690
 
5691
+ ## Drive KiCI from your coding agent
5692
+
5693
+ Source: https://docs.kici.dev/user/ai-agents/
5694
+
5695
+ KiCI ships a hosted **MCP server** so a coding agent (Claude Code, or any MCP
5696
+ client) can drive your CI directly: trigger runs, read a structured result,
5697
+ fetch the failing step's logs, cancel, and re-run — all under your own identity,
5698
+ org-scoped, and audited. There are no per-tool tokens to configure: point the
5699
+ agent at one URL with one credential and it's done.
5700
+
5701
+ The MCP exposes only what you can already do yourself through the `kici` CLI and
5702
+ the dashboard. It is not a new privileged surface — every tool maps to an
5703
+ existing user-facing operation and is gated by the same permissions your role
5704
+ grants.
5705
+
5706
+ ## 1. Mint an agent token
5707
+
5708
+ The MCP accepts **only** an agent-kind personal access token (PAT). Mint one with
5709
+ the `kici` CLI (log in first with `kici login`):
5710
+
5711
+ ```bash
5712
+ kici pat create --agent --name "claude-code"
5713
+ ```
5714
+
5715
+ The `--name` value is the **agent label**. It is recorded on every action the
5716
+ agent takes, so your audit log shows exactly which agent did what (and on whose
5717
+ behalf). The token is printed once — save it now; it cannot be retrieved later.
5718
+
5719
+ An agent PAT inherits your permissions unchanged — it carries provenance, not
5720
+ extra authority. Powerful operator capabilities (secret rotation, agent and peer
5721
+ management, draining) are intentionally **not** exposed here.
5722
+
5723
+ ## 2. Point your coding agent at the MCP server
5724
+
5725
+ Configure your MCP client with the KiCI MCP endpoint and the agent PAT as a
5726
+ Bearer credential. The endpoint is the hosted Platform URL plus `/api/v1/mcp`.
5727
+
5728
+ For Claude Code, add a remote MCP server whose URL is your KiCI Platform's
5729
+ `/api/v1/mcp` and whose `Authorization` header is `Bearer <your-agent-pat>`.
5730
+
5731
+ That's the entire setup. The agent can now call the tools below.
5732
+
5733
+ ## 3. What the agent can do
5734
+
5735
+ **Read**
5736
+
5737
+ - `list_runs` — recent runs in your organization.
5738
+ - `get_run` — the structured, provenance-tagged result of a run: the typed job
5739
+ graph, per-step statuses and exit codes, durations, and a derived failure
5740
+ category.
5741
+ - `get_step_logs` — the log lines for a specific step.
5742
+ - `list_workflows` — your registered workflows.
5743
+
5744
+ **Drive**
5745
+
5746
+ - `trigger_run` — run a registered workflow ("run now").
5747
+ - `rerun_run` — re-run a completed run.
5748
+ - `cancel_run` — cancel an in-progress run.
5749
+
5750
+ If you belong to a single organization, the org is resolved automatically. If
5751
+ you belong to several, pass an `orgId` argument to any tool.
5752
+
5753
+ ## 4. Why the structured result is agent-safe
5754
+
5755
+ `get_run` and `get_step_logs` return a machine-first shape designed for an agent
5756
+ to reason over without being misled by repository content. Every field that
5757
+ comes from your repo, a contributor, or a process's output — workflow and job
5758
+ names, refs, error messages, log lines, job outputs — is wrapped in an
5759
+ `{ untrusted: true, value: … }` envelope. KiCI-generated values (ids, statuses,
5760
+ exit codes, durations, the derived failure category) are left plain. An agent can
5761
+ keep user-controlled content out of its instruction channel by refusing to act
5762
+ on anything tagged `untrusted`.
5763
+
5764
+ Secret values are never returned — only the names of the secret keys a step
5765
+ accessed.
5766
+
5767
+ ## 5. The audit guarantee
5768
+
5769
+ Because the MCP accepts only an agent-kind PAT, **every action that flows through
5770
+ it is agent-attributed by construction** — there is no path that produces an
5771
+ untagged, human-looking action. Each read and each drive operation is recorded in
5772
+ your orchestrator's access log under your identity plus the agent label, so you
5773
+ always have a complete trail of what your agent did.
5774
+
5775
+ Inspect that trail with `kici-admin access-log list --json` (or
5776
+ `kici-admin access-log show <id>` for one entry). An agent-attributed row keeps
5777
+ `actor_type` as `user` and `actor_id` as your own identity — the agent provenance
5778
+ rides in the row's actor metadata as `agentLabel` (the `--name` you minted the
5779
+ PAT with) and `agentPatId` (the token that acted). The label is also stored in a
5780
+ dedicated `agent_label` column on every such row.
5781
+
5782
+ ---
5783
+
5302
5784
  ## CLI authentication
5303
5785
 
5304
5786
  Source: https://docs.kici.dev/user/cli-auth/
@@ -5458,13 +5940,13 @@ Every request to `/api/v1/*` carries an `Authorization: Bearer <token>` header.
5458
5940
  | `kici_sa_` | Service account key | Dashboard → Settings → Service accounts | Org |
5459
5941
  | (other) | OIDC JWT or opaque OIDC token | OIDC login (browser SPA) | User (cross-org) |
5460
5942
 
5461
- JWT and opaque OIDC tokens are validated against the configured OIDC issuer (JWKS for JWTs, the issuer's UserInfo endpoint for opaque ones). All `kici_*` tokens are validated by SHA-256 hash lookup against the Platform DB. See [RBAC: authentication methods](../architecture/security/rbac.md#authentication-methods) for the full model.
5943
+ JWT and opaque OIDC tokens are validated against the configured OIDC issuer (JWKS for JWTs, the issuer's UserInfo endpoint for opaque ones). All `kici_*` tokens are validated by SHA-256 hash lookup against the Platform DB. See [RBAC: authentication methods](https://docs.kici.dev/architecture/security/rbac/#authentication-methods) for the full model.
5462
5944
 
5463
5945
  > **Note:** `kici_ok_` keys are **not** for the HTTP API — they authenticate orchestrator-to-Platform WebSocket connections only. Use `kici_sk_` (or `kici_pat_`) for HTTP calls.
5464
5946
 
5465
5947
  ### Permissions
5466
5948
 
5467
- Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The 15 resources and 5 levels are documented in [RBAC](../architecture/security/rbac.md#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
5949
+ Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The 17 resources and 5 levels are documented in [RBAC](https://docs.kici.dev/architecture/security/rbac/#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
5468
5950
 
5469
5951
  ### Configurable surfaces
5470
5952
 
@@ -5509,7 +5991,7 @@ There is currently no per-token rate limit on `/api/v1/*`. A single global body-
5509
5991
 
5510
5992
  ### Audit trail
5511
5993
 
5512
- Every `/api/v1/*` mutation that touches tenant-plane data is recorded in the upstream tenant-plane audit log, stamped with the actor (user, API key, service account, or upstream operator on a break-glass support read). Reads on customer data go through the orchestrator over the WebSocket proxy and land in the orchestrator's `access_log` table. See [Audit log](../operator/security/audit-log.md) for the orchestrator schema and the dashboard's "Activity" page for the federated view.
5994
+ Every `/api/v1/*` mutation that touches tenant-plane data is recorded in the upstream tenant-plane audit log, stamped with the actor (user, API key, service account, or upstream operator on a break-glass support read). Reads on customer data go through the orchestrator over the WebSocket proxy and land in the orchestrator's `access_log` table. See [Audit log](https://docs.kici.dev/operator/security/audit-log/) for the orchestrator schema and the dashboard's "Activity" page for the federated view.
5513
5995
 
5514
5996
  ## Token storage
5515
5997
 
@@ -5581,7 +6063,7 @@ Run commands with `npx kici` or add scripts to your `package.json`:
5581
6063
  {
5582
6064
  "scripts": {
5583
6065
  "kici:compile": "kici compile",
5584
- "kici:test": "kici test"
6066
+ "kici:preview": "kici preview"
5585
6067
  }
5586
6068
  }
5587
6069
  ```
@@ -5655,26 +6137,29 @@ kici run local [event] [options]
5655
6137
 
5656
6138
  **Options:**
5657
6139
 
5658
- | Option | Default | Description |
5659
- | ------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5660
- | `-p, --pick` | `false` | Interactively pick a workflow + trigger (see below) |
5661
- | `--workflow <name>` | none | Run only the specified workflow (mutex with `--pick`) |
5662
- | `--job <name>` | none | Run only the specified job (and its dependencies) |
5663
- | `--branch <name>` | detected | Override detected git branch |
5664
- | `--sha <hash>` | detected | Override detected git SHA |
5665
- | `--payload <path>` | none | Path to explicit event payload JSON file |
5666
- | `--concurrency <n>` | CPU cores | Max parallel jobs **within one run** (job-level only). Cross-run [concurrency groups](concurrency.md) declared in `workflow({ concurrency: ... })` are enforced separately — see "Concurrency enforcement" below. |
5667
- | `--keep-going` | `false` | Continue after job failure |
5668
- | `--container` | `false` | Use Podman container isolation |
5669
- | `--env <KEY=VALUE>` | none | Environment variable override (repeatable) |
5670
- | `--files <path>` | git diff | Override changed file paths (repeatable, default: git diff) |
5671
- | `--quiet` | `false` | Suppress streaming output (summary only) |
5672
- | `--json` | `false` | Output structured JSON result |
5673
- | `--junit <path>` | none | Output JUnit XML result to file |
5674
- | `--debug` | `false` | Verbose internals |
5675
- | `--kici-dir <path>` | `.kici` | Path to .kici directory |
5676
- | `--in-place` | `false` | Run against the real working directory instead of an isolated tmp checkout (see "Execution isolation" below) |
5677
- | `--keep` | `false` | Always retain the isolated tmp checkout (default: keep only on failure) |
6140
+ | Option | Default | Description |
6141
+ | --------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6142
+ | `-p, --pick` | `false` | Interactively pick a workflow + trigger (see below) |
6143
+ | `--workflow <name>` | none | Run only the specified workflow (mutex with `--pick`) |
6144
+ | `--job <name>` | none | Run only the specified job (and its dependencies) |
6145
+ | `--branch <name>` | detected | Override detected git branch |
6146
+ | `--sha <hash>` | detected | Override detected git SHA |
6147
+ | `--payload <path>` | none | Path to explicit event payload JSON file |
6148
+ | `--concurrency <n>` | CPU cores | Max parallel jobs **within one run** (job-level only). Cross-run [concurrency groups](https://docs.kici.dev/user/concurrency/) declared in `workflow({ concurrency: ... })` are enforced separately — see "Concurrency enforcement" below. |
6149
+ | `--keep-going` | `false` | Continue after job failure |
6150
+ | `--container` | `false` | Use Podman container isolation |
6151
+ | `--env <KEY=VALUE>` | none | Environment variable override (repeatable) |
6152
+ | `--input <KEY=VALUE>` | none | Typed workflow-dispatch input (repeatable) — coerced + validated against the workflow's `dispatch({ inputs })` schema, exposed as `ctx.dispatchInputs` (see [triggers → typed dispatch inputs](https://docs.kici.dev/user/sdk/triggers/#typed-dispatch-inputs)) |
6153
+ | `--files <path>` | git diff | Override changed file paths (repeatable, default: git diff) |
6154
+ | `--quiet` | `false` | Suppress streaming output (summary only) |
6155
+ | `--json` | `false` | Output structured JSON result |
6156
+ | `--junit <path>` | none | Output JUnit XML result to file |
6157
+ | `--debug` | `false` | Verbose internals |
6158
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
6159
+ | `--in-place` | `false` | Run against the real working directory instead of an isolated tmp checkout (see "Execution isolation" below) |
6160
+ | `--keep` | `false` | Always retain the isolated tmp checkout (default: keep only on failure) |
6161
+ | `--check` | `false` | Run in [check mode](https://docs.kici.dev/user/idempotent-steps/): each step reports drift instead of applying; the run still exits 0 |
6162
+ | `--fail-on-drift` | `false` | In check mode, exit non-zero if any step reports drift (no effect without `--check`) |
5678
6163
 
5679
6164
  **Interactive workflow selection (`--pick` / `-p`):**
5680
6165
 
@@ -5778,9 +6263,11 @@ kici run local push --keep-going
5778
6263
 
5779
6264
  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.
5780
6265
 
5781
- 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.
6266
+ 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](https://docs.kici.dev/user/cli-reference/#how-the-run-is-routed) and [The two planes](https://docs.kici.dev/user/cli-reference/#the-two-planes) below.
6267
+
6268
+ Like `kici run local`, `kici run remote` recompiles your workflows (`.kici/workflows` → `kici.lock.json`) before dispatching, so the orchestrator matches and dispatches against your current workflow definitions — a brand-new or edited workflow takes effect without a separate `kici compile`. A compile or validation error aborts the run before anything is uploaded.
5782
6269
 
5783
- 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.
6270
+ 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](https://docs.kici.dev/user/testing-guide/) and [Storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/) for setup.
5784
6271
 
5785
6272
  ```bash
5786
6273
  kici run remote [fixture] [options]
@@ -5794,24 +6281,30 @@ kici run remote [fixture] [options]
5794
6281
 
5795
6282
  **Options:**
5796
6283
 
5797
- | Option | Default | Description |
5798
- | --------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
5799
- | `--org <id>` | active | Target organization for this run (overrides `kici org use`) |
5800
- | `--orchestrator <name>` | default | Target orchestrator cluster within the org (overrides the per-org default) |
5801
- | `--all` | `false` | Run all fixtures |
5802
- | `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
5803
- | `--parallel` | `false` | Run multiple fixtures concurrently |
5804
- | `--no-wait` | - | Fire and forget (print runIds, don't stream) |
5805
- | `--quiet` | `false` | Minimal output (only final result) |
5806
- | `--json` | `false` | Machine-readable JSON output |
5807
- | `--junit <path>` | none | JUnit XML output to file for CI integration |
5808
- | `--history` | `false` | Show table of recent test runs |
5809
- | `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted (repeatable) |
5810
- | `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted (repeatable) — see [testing guide](testing-guide.md) |
5811
- | `--target <selector>` | none | Narrow `runsOnAll` jobs to hosts matching this label selector (repeatable, AND-combined) |
5812
- | `--target-allow-empty` | `false` | A `--target` that narrows a `runsOnAll` job to zero hosts skips it instead of failing |
5813
- | `--debug` | `false` | Verbose internals |
5814
- | `--kici-dir <path>` | `.kici` | Path to .kici directory |
6284
+ | Option | Default | Description |
6285
+ | --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
6286
+ | `--org <id>` | active | Target organization for this run (overrides `kici org use`) |
6287
+ | `--orchestrator <name>` | default | Target orchestrator cluster within the org (overrides the per-org default) |
6288
+ | `--all` | `false` | Run all fixtures |
6289
+ | `-p, --pick` | `false` | Interactively pick fixtures to run (multi-select; mutex with a fixture arg, `--all`, and `--workflow`) |
6290
+ | `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
6291
+ | `--parallel` | `false` | Run multiple fixtures concurrently |
6292
+ | `--no-wait` | - | Fire and forget (print runIds, don't stream) |
6293
+ | `--quiet` | `false` | Minimal output (only final result) |
6294
+ | `--json` | `false` | Machine-readable JSON output |
6295
+ | `--junit <path>` | none | JUnit XML output to file for CI integration |
6296
+ | `--history` | `false` | Show table of recent test runs |
6297
+ | `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted (repeatable) |
6298
+ | `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted (repeatable) see [testing guide](https://docs.kici.dev/user/testing-guide/) |
6299
+ | `--input <KEY=VALUE>` | none | Typed workflow-dispatch input (repeatable) validated + coerced + defaulted on the orchestrator from the lock descriptor, exposed as `ctx.dispatchInputs` (see [triggers typed dispatch inputs](https://docs.kici.dev/user/sdk/triggers/#typed-dispatch-inputs)) |
6300
+ | `--target <selector>` | none | Narrow `runsOnAll` jobs to hosts matching this label selector (repeatable, AND-combined) |
6301
+ | `--target-allow-empty` | `false` | A `--target` that narrows a `runsOnAll` job to zero hosts skips it instead of failing |
6302
+ | `--debug` | `false` | Verbose internals |
6303
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
6304
+ | `--routing-key <key>` | none | Override the routing key for this run (advanced; selecting the org normally suffices — see [How the run is routed](https://docs.kici.dev/user/cli-reference/#how-the-run-is-routed)) |
6305
+ | `--check` | `false` | Run in [check mode](https://docs.kici.dev/user/idempotent-steps/): each step reports drift instead of applying |
6306
+ | `--fail-on-drift` | `false` | In check mode, fail the run if any step reports drift (no effect without `--check`) |
6307
+ | `--approve-all, --yes` | `false` | Auto-approve every [approval gate](https://docs.kici.dev/user/approvals/) this run holds on (run-scoped; eligibility still enforced) |
5815
6308
 
5816
6309
  **Examples:**
5817
6310
 
@@ -5849,6 +6342,9 @@ kici run remote push-main --no-wait
5849
6342
  # View recent test run history
5850
6343
  kici run remote --history
5851
6344
 
6345
+ # Interactively pick which fixtures to run (multi-select)
6346
+ kici run remote --pick
6347
+
5852
6348
  # Narrow runsOnAll jobs to a subset of the host roster
5853
6349
  kici run remote deploy --target role:web
5854
6350
 
@@ -5859,6 +6355,18 @@ kici run remote deploy --target role:web --target dc:eu
5859
6355
  kici run remote deploy --target role:gpu --target-allow-empty
5860
6356
  ```
5861
6357
 
6358
+ **Interactive fixture selection (`--pick` / `-p`):**
6359
+
6360
+ Pass `--pick` (or `-p`) to open an interactive checkbox menu of the available
6361
+ fixtures. Toggle one or more with space, confirm with enter, and the selected
6362
+ fixtures run through the normal remote pipeline (honoring `--parallel`,
6363
+ `--no-wait`, and the other run flags). Notes:
6364
+
6365
+ - `--pick` is mutually exclusive with a fixture argument, `--all`, and
6366
+ `--workflow`. Passing any together exits with code 2.
6367
+ - When `stdin` is not a TTY, `--pick` prints the available fixtures and exits
6368
+ without running anything — pass a fixture name (or `--all`) in scripts.
6369
+
5862
6370
  #### Host narrowing with `--target`
5863
6371
 
5864
6372
  `--target <selector>` is a runtime narrowing for `runsOnAll` jobs, analogous to
@@ -5883,7 +6391,7 @@ When `--target` narrows a `runsOnAll` job to zero hosts, the default is to **fai
5883
6391
  the run (fail-loud — a typo in the selector shouldn't silently skip work). Pass
5884
6392
  `--target-allow-empty` to **skip** the zeroed job instead; the job records a
5885
6393
  `skipped` status, and any downstream job that needs it with `when: 'on-skip'` (or
5886
- `when: 'always'`) still runs. See [Job dependencies](./sdk/core.md#job-dependencies-needs)
6394
+ `when: 'always'`) still runs. See [Job dependencies](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs)
5887
6395
  for the `when` gating model.
5888
6396
 
5889
6397
  **Exit codes:**
@@ -5915,9 +6423,9 @@ When an org has more than one connected orchestrator cluster, the CLI picks the
5915
6423
  `kici run remote` uses two independent paths:
5916
6424
 
5917
6425
  - **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.
5918
- - **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).
6426
+ - **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](https://docs.kici.dev/operator/orchestrator/storage-layout/).
5919
6427
 
5920
- 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).
6428
+ 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`](https://docs.kici.dev/user/cli-reference/#kici-run-local).
5921
6429
 
5922
6430
  #### Fresh repos (no GitHub remote)
5923
6431
 
@@ -5931,7 +6439,7 @@ An orchestrator with no Platform connection cannot serve remote runs — the Pla
5931
6439
 
5932
6440
  Destination routing is unchanged for fresh repos: the run still goes to your active org through the Platform.
5933
6441
 
5934
- For a detailed guide on writing fixtures, configuring secrets, and understanding the upload flow, see [Testing guide](testing-guide.md).
6442
+ For a detailed guide on writing fixtures, configuring secrets, and understanding the upload flow, see [Testing guide](https://docs.kici.dev/user/testing-guide/).
5935
6443
 
5936
6444
  #### kici orchestrators
5937
6445
 
@@ -5962,12 +6470,12 @@ kici orchestrators use us-east
5962
6470
  kici orchestrators use us-east --org xyz789ghi012
5963
6471
  ```
5964
6472
 
5965
- ### kici test
6473
+ ### kici preview
5966
6474
 
5967
6475
  Preview which workflows match a trigger event (dry-run, no execution). Useful for verifying trigger configurations during development.
5968
6476
 
5969
6477
  ```bash
5970
- kici test [event] [options]
6478
+ kici preview [event] [options]
5971
6479
  ```
5972
6480
 
5973
6481
  **Arguments:**
@@ -5994,19 +6502,19 @@ kici test [event] [options]
5994
6502
 
5995
6503
  ```bash
5996
6504
  # Preview which workflows match a push event
5997
- kici test push
6505
+ kici preview push
5998
6506
 
5999
6507
  # Preview PR trigger matching
6000
- kici test pr:open
6508
+ kici preview pr:open
6001
6509
 
6002
6510
  # Preview with branch override
6003
- kici test push --branch develop
6511
+ kici preview push --branch develop
6004
6512
 
6005
6513
  # Filter to specific workflow
6006
- kici test push --workflow ci
6514
+ kici preview push --workflow ci
6007
6515
 
6008
6516
  # Simulate changed files for path-filtered triggers
6009
- kici test push --files src/index.ts --files README.md
6517
+ kici preview push --files src/index.ts --files README.md
6010
6518
  ```
6011
6519
 
6012
6520
  **Exit codes:**
@@ -6016,7 +6524,7 @@ kici test push --files src/index.ts --files README.md
6016
6524
  | 0 | Preview completed (including zero matches) |
6017
6525
  | 1 | Error |
6018
6526
 
6019
- **Migration from old `kici test <fixture>`:** If you were using `kici test <fixture-name>` for remote fixture execution, use `kici run remote <fixture-name>` instead. For local workflow execution, use `kici run local <event>`.
6527
+ **Migration from the old `test` command:** The dry-run preview command was renamed from `test` to `preview`. If you were using the old `test` command with a fixture name for remote fixture execution, use `kici run remote <fixture-name>` instead. For local workflow execution, use `kici run local <event>`.
6020
6528
 
6021
6529
  ### kici login
6022
6530
 
@@ -6316,7 +6824,7 @@ for `--quiet`.
6316
6824
 
6317
6825
  ### kici approve
6318
6826
 
6319
- 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.
6827
+ Approve a held [approval gate](https://docs.kici.dev/user/approvals/) so the run resumes. Identify the held element by run ID, optionally narrowed to a job and step.
6320
6828
 
6321
6829
  ```bash
6322
6830
  kici approve <run-id> [options]
@@ -6352,7 +6860,7 @@ You must be eligible for at least one unsatisfied clause (a member of a named te
6352
6860
 
6353
6861
  ### kici reject
6354
6862
 
6355
- Reject a held [approval gate](approvals.md). A rejection fails the held element and the run. A reason is required.
6863
+ Reject a held [approval gate](https://docs.kici.dev/user/approvals/). A rejection fails the held element and the run. A reason is required.
6356
6864
 
6357
6865
  ```bash
6358
6866
  kici reject <run-id> --reason <text> [options]
@@ -6393,6 +6901,27 @@ Only key names are shown — secret values are never returned over this endpoint
6393
6901
 
6394
6902
  **Prerequisites:** authenticate via `kici login` and select an active organization with `kici org use <name>`.
6395
6903
 
6904
+ ### kici pat create
6905
+
6906
+ Mint a personal access token under your own identity. Pass `--agent` to mint an
6907
+ **agent-kind** PAT — the credential a coding agent points the KiCI MCP server at.
6908
+
6909
+ ```bash
6910
+ kici pat create --agent --name "claude-code"
6911
+ ```
6912
+
6913
+ - `--agent` marks the token as agent-kind. An agent PAT inherits your
6914
+ permissions unchanged (it carries provenance, not extra authority) and is the
6915
+ **only** credential the MCP server accepts.
6916
+ - `--name <label>` sets the token name. For an agent PAT this is the **agent
6917
+ label** recorded on every action the agent takes — required with `--agent`.
6918
+ - `--expires-in-days <n>` overrides the default expiry.
6919
+
6920
+ The token is printed once — save it immediately; it cannot be retrieved later.
6921
+ See [Drive KiCI from your coding agent](https://docs.kici.dev/user/ai-agents/) for the full setup.
6922
+
6923
+ **Prerequisites:** authenticate via `kici login` first.
6924
+
6396
6925
  ### kici types
6397
6926
 
6398
6927
  Generate TypeScript declaration files from orchestrator environment metadata. The generated `.d.ts` file augments the SDK's `KnownSecretKeys` and `EnvironmentSecrets` interfaces, providing compile-time autocomplete and type checking for secret key names.
@@ -6431,7 +6960,7 @@ After generating types, `ctx.secrets.get('MY_KEY')` and `ctx.secrets.expose('DB_
6431
6960
 
6432
6961
  **Git workflow:** Commit the generated `.kici/types/secrets.d.ts` so team members get type checking without needing orchestrator access. Run `kici types` to refresh when environments change.
6433
6962
 
6434
- **Auto-regeneration:** `kici compile` automatically runs `kici types` after successful compilation when authenticated. See the [kici compile](#kici-compile) section for details.
6963
+ **Auto-regeneration:** `kici compile` automatically runs `kici types` after successful compilation when authenticated. See the [kici compile](https://docs.kici.dev/user/cli-reference/#kici-compile) section for details.
6435
6964
 
6436
6965
  **Escape hatch:** For dynamic keys not in the generated types, use a cast: `(ctx.secrets as any).DYNAMIC_KEY`.
6437
6966
 
@@ -6650,9 +7179,10 @@ Open the KiCI documentation site in the default browser. With the `llm` subcomma
6650
7179
  ```bash
6651
7180
  kici docs # open https://kici.dev/docs/
6652
7181
  kici docs --no-open # print the URL instead of opening a browser
6653
- kici docs llm # print llms-full.txt (the full bundle) to stdout
6654
- kici docs llm --index # print llms.txt (the curated link index) to stdout
6655
- kici docs llm --out path/to/file.md # write the bundle to a file
7182
+ kici docs llm # print the llms.txt index (a router over the task bundles)
7183
+ kici docs llm sdk # print the SDK task bundle
7184
+ kici docs llm full # print llms-full.txt (every page in one file)
7185
+ kici docs llm sdk --out sdk-context.md # write a bundle to a file
6656
7186
  ```
6657
7187
 
6658
7188
  **Examples:**
@@ -6661,14 +7191,14 @@ kici docs llm --out path/to/file.md # write the bundle to a file
6661
7191
  # Open the docs site in your browser
6662
7192
  kici docs
6663
7193
 
6664
- # Pipe the full LLM bundle into a coding agent
6665
- kici docs llm | claude -- "Read this and help me author a deploy workflow"
7194
+ # Pipe just the SDK bundle into a coding agent (small, task-scoped context)
7195
+ kici docs llm sdk | claude -- "Read this and help me author a deploy workflow"
6666
7196
 
6667
- # Save the curated index for offline reference
6668
- kici docs llm --index --out kici-llms.txt
7197
+ # Save the router index for offline reference
7198
+ kici docs llm --out kici-llms-index.txt
6669
7199
  ```
6670
7200
 
6671
- The bundle is regenerated from `docs/` every time `@kici-dev/compiler` is built, so it always matches your installed CLI version. The same content is available online at <https://kici.dev/llms.txt> and <https://kici.dev/llms-full.txt> following the [llms.txt convention](https://llmstxt.org/).
7201
+ Bundles are regenerated from `docs/` every time `@kici-dev/compiler` is built, so they always match your installed CLI version. The index lists each task bundle `getting-started`, `sdk`, `cli`, `patterns`, `features`, `providers`, `architecture` — with its size and a one-line purpose; pass the bundle id as the topic. Every cross-reference link inside a bundle is an absolute `docs.kici.dev` URL. The same files are published online following the [llms.txt convention](https://llmstxt.org/).
6672
7202
 
6673
7203
  ### kici admin
6674
7204
 
@@ -6707,10 +7237,10 @@ kici admin drain-worker --url http://worker-2.internal:10143
6707
7237
 
6708
7238
  ### kici verify-attestation
6709
7239
 
6710
- 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.
7240
+ 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](https://docs.kici.dev/user/provenance/). 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.
6711
7241
 
6712
7242
  ```bash
6713
- kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-file> [options]
7243
+ kici verify-attestation [artifact] --bundle <path-or-url> [--trust-root <url-or-file>] [options]
6714
7244
  ```
6715
7245
 
6716
7246
  **Arguments:**
@@ -6721,14 +7251,14 @@ kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-f
6721
7251
 
6722
7252
  **Options:**
6723
7253
 
6724
- | Option | Required | Description |
6725
- | ---------------------------- | -------- | ----------------------------------------------------------------------------------------- |
6726
- | `--bundle <path-or-url>` | yes | Path or `http(s)` URL to the attestation bundle JSON. |
6727
- | `--trust-root <url-or-file>` | yes | Trusted issuer (see below). The token issuer is pinned to it, never taken from the token. |
6728
- | `--audience <aud>` | no | Expected token audience (defaults to the KiCI provenance audience). |
6729
- | `--json` | no | Print the structured verification result as JSON instead of human-readable output. |
7254
+ | Option | Required | Description |
7255
+ | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
7256
+ | `--bundle <path-or-url>` | yes | Path or `http(s)` URL to the attestation bundle JSON. |
7257
+ | `--trust-root <url-or-file>` | no | Trusted issuer (see below). Defaults to the hosted KiCI platform. The token issuer is pinned to it, never taken from the token. |
7258
+ | `--audience <aud>` | no | Expected token audience (defaults to the KiCI provenance audience). |
7259
+ | `--json` | no | Print the structured verification result as JSON instead of human-readable output. |
6730
7260
 
6731
- **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:
7261
+ **Trust root:** `--trust-root` defaults to the hosted KiCI platform's provenance issuer — the same platform you `kici login` against (see [Which trust root do I use?](https://docs.kici.dev/user/provenance/#which-trust-root-do-i-use)), so the common case needs no flag. The verifier never trusts the issuer named inside the token; supplying it out-of-band is what prevents a forged bundle from self-attesting. To override the default, pass `--trust-root` in one of two forms:
6732
7262
 
6733
7263
  - **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`.
6734
7264
  - **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):
@@ -6747,7 +7277,10 @@ kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-f
6747
7277
  **Examples:**
6748
7278
 
6749
7279
  ```bash
6750
- # Online: verify a bundle against a deployed issuer, digest-checking the artifact
7280
+ # Default: verify against the hosted KiCI platform (no --trust-root needed)
7281
+ kici verify-attestation ./dist/app.tgz --bundle ./app.tgz.kici.json
7282
+
7283
+ # Override: verify a bundle against a specific issuer, digest-checking the artifact
6751
7284
  kici verify-attestation ./dist/app.tgz \
6752
7285
  --bundle ./app.tgz.kici.json \
6753
7286
  --trust-root https://platform.example/issuer
@@ -6764,10 +7297,10 @@ kici verify-attestation --bundle ./app.tgz.kici.json \
6764
7297
 
6765
7298
  **Exit codes:**
6766
7299
 
6767
- | Code | Meaning |
6768
- | ---- | ----------------------------------------------------------------------------------- |
6769
- | 0 | Verified — signature, identity, build context (and digest, if checked) all pass |
6770
- | 1 | Not verified, or an error (missing flag, unreadable bundle, unreachable trust root) |
7300
+ | Code | Meaning |
7301
+ | ---- | ----------------------------------------------------------------------------------------- |
7302
+ | 0 | Verified — signature, identity, build context (and digest, if checked) all pass |
7303
+ | 1 | Not verified, or an error (missing `--bundle`, unreadable bundle, unreachable trust root) |
6771
7304
 
6772
7305
  ## Workflow discovery
6773
7306
 
@@ -6796,7 +7329,7 @@ The `kici compile` command produces `.kici/kici.lock.json` inside the `.kici` di
6796
7329
  - Should be committed to version control
6797
7330
  - Is regenerated on every `kici compile` run
6798
7331
 
6799
- Use `kici compile --check` in CI to validate that workflows are correct without writing files. For the full story on drift, pre-commit/CI, and agent-side verification, see [Lock file and workflow drift](lock-file-and-drift.md).
7332
+ Use `kici compile --check` in CI to validate that workflows are correct without writing files. For the full story on drift, pre-commit/CI, and agent-side verification, see [Lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/).
6800
7333
 
6801
7334
  ## Exit codes
6802
7335
 
@@ -6809,7 +7342,7 @@ All commands follow a consistent exit code convention:
6809
7342
 
6810
7343
  ## Debug output
6811
7344
 
6812
- Use `--debug` (on `kici run local`, `kici run remote`, `kici test`) or `--verbose` (on `kici compile`) for detailed output:
7345
+ Use `--debug` (on `kici run local`, `kici run remote`, `kici preview`) or `--verbose` (on `kici compile`) for detailed output:
6813
7346
 
6814
7347
  ```bash
6815
7348
  # Shows trigger matching, rule evaluation, decision traces
@@ -6819,7 +7352,7 @@ kici run local push --debug
6819
7352
  kici compile --verbose
6820
7353
 
6821
7354
  # Shows trigger matching preview
6822
- kici test pr:open --debug
7355
+ kici preview pr:open --debug
6823
7356
  ```
6824
7357
 
6825
7358
  Set `KICI_DEBUG=true` for additional internal debug output across all commands.
@@ -6834,10 +7367,10 @@ Set `KICI_DEBUG=true` for additional internal debug output across all commands.
6834
7367
 
6835
7368
  ## See also
6836
7369
 
6837
- - [Getting started](getting-started.md) -- install the SDK and write your first workflow
6838
- - [Testing guide](testing-guide.md) -- writing fixtures, remote test runs, secret contexts, and repo state transfer
6839
- - [SDK reference](sdk-reference.md) -- complete API for the workflow definitions that the CLI compiles
6840
- - [Workflow patterns](workflow-patterns.md) -- example workflows to compile and test with these commands
7370
+ - [Getting started](https://docs.kici.dev/user/getting-started/) -- install the SDK and write your first workflow
7371
+ - [Testing guide](https://docs.kici.dev/user/testing-guide/) -- writing fixtures, remote test runs, secret contexts, and repo state transfer
7372
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) -- complete API for the workflow definitions that the CLI compiles
7373
+ - [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/) -- example workflows to compile and test with these commands
6841
7374
 
6842
7375
  ---
6843
7376
 
@@ -7100,7 +7633,7 @@ The lock file (`kici.lock.json`) is a JSON file with the following top-level fie
7100
7633
 
7101
7634
  | Field | Description |
7102
7635
  | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7103
- | `schemaVersion` | Lock file schema version (currently 20). Incremented on breaking format changes. |
7636
+ | `schemaVersion` | Lock file schema version (currently 29). Incremented on breaking format changes. |
7104
7637
  | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
7105
7638
  | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
7106
7639
  | `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. |
@@ -7118,12 +7651,12 @@ Each workflow entry includes:
7118
7651
  | `jobs` | Job definitions with scheduling metadata (runsOn, needs, matrix, environment, concurrency, container, checkout, gracePeriod, label routing, dynamic fields, etc.). |
7119
7652
  | `rules` | Workflow-level conditional rules (optional). Stored as dynamic references since rule functions cannot be serialized. |
7120
7653
  | `description` | Optional workflow description. |
7121
- | `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](#extra-files-in-the-content-hash-hashfiles). |
7654
+ | `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](https://docs.kici.dev/user/lock-file-and-drift/#extra-files-in-the-content-hash-hashfiles). |
7122
7655
  | `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
7123
7656
  | `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
7124
- | `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](private-registries.md). |
7125
- | `installEnv` | Extra qualified secret refs (`<environment>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](private-registries.md). |
7126
- | `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](concurrency.md). |
7657
+ | `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](https://docs.kici.dev/user/private-registries/). |
7658
+ | `installEnv` | Extra qualified secret refs (`<environment>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](https://docs.kici.dev/user/private-registries/). |
7659
+ | `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
7127
7660
  | `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
7128
7661
  | Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
7129
7662
 
@@ -7152,7 +7685,7 @@ Install a hook that compiles and stages the lock file before each commit:
7152
7685
  npx kici hook install
7153
7686
  ```
7154
7687
 
7155
- This runs `kici compile && git add .kici/kici.lock.json` before each commit: if compilation fails the commit is blocked; if it succeeds the updated lock file is automatically staged. See [CLI Reference — kici hook](cli-reference.md#kici-hook) for options (husky, lefthook, pre-commit, prek, raw git).
7688
+ This runs `kici compile && git add .kici/kici.lock.json` before each commit: if compilation fails the commit is blocked; if it succeeds the updated lock file is automatically staged. See [CLI Reference — kici hook](https://docs.kici.dev/user/cli-reference/#kici-hook) for options (husky, lefthook, pre-commit, prek, raw git).
7156
7689
 
7157
7690
  ### CI check
7158
7691
 
@@ -7202,9 +7735,9 @@ So even without a pre-commit or CI check, a stale lock file will cause the run t
7202
7735
 
7203
7736
  ## See also
7204
7737
 
7205
- - [Getting Started](getting-started.md) — compile and commit the lock file
7206
- - [CLI Reference](cli-reference.md) — `kici compile`, `kici compile --check`, `kici hook`
7207
- - [Architecture — Data flows](../architecture/data-flows.md) — how the lock file is used in the pipeline
7738
+ - [Getting Started](https://docs.kici.dev/user/getting-started/) — compile and commit the lock file
7739
+ - [CLI Reference](https://docs.kici.dev/user/cli-reference/) — `kici compile`, `kici compile --check`, `kici hook`
7740
+ - [Architecture — Data flows](https://docs.kici.dev/architecture/data-flows/) — how the lock file is used in the pipeline
7208
7741
 
7209
7742
  ---
7210
7743
 
@@ -7223,17 +7756,17 @@ Test your workflows remotely against the full CI pipeline from your local machin
7223
7756
  - Give test runs test-scoped secrets — your local secret files and `--env` values (uploaded encrypted) plus any environment flagged `allowLocalExecution: true` — while production environments stay unreachable
7224
7757
  - Detect test mode in workflow code via `ctx.isTestRun`
7225
7758
 
7226
- The command is remote-only -- all execution happens on the orchestrator and agent. For local-only trigger matching previews, use `kici test <event>`.
7759
+ The command is remote-only -- all execution happens on the orchestrator and agent. For local-only trigger matching previews, use `kici preview <event>`.
7227
7760
 
7228
7761
  :::note[Orchestrator prerequisite: cache storage]
7229
- `kici run remote` uploads your working-tree overlay to the orchestrator's **cache storage** via a pre-signed URL, and the agent fetches it from there (see [Repo state transfer](#repo-state-transfer)). The target orchestrator must therefore have cache storage enabled (`KICI_STORAGE_TYPE` = `s3` or `filesystem`).
7762
+ `kici run remote` uploads your working-tree overlay to the orchestrator's **cache storage** via a pre-signed URL, and the agent fetches it from there (see [Repo state transfer](https://docs.kici.dev/user/testing-guide/#repo-state-transfer)). The target orchestrator must therefore have cache storage enabled (`KICI_STORAGE_TYPE` = `s3` or `filesystem`).
7230
7763
 
7231
- - **Both quickstarts wire this up for you** — the [Docker / Podman quickstart](quickstart/compose.md) and the [bare-metal quickstart](quickstart/bare-metal.md) each ship a SeaweedFS object store and pre-fill the orchestrator's `KICI_STORAGE_*` block, so `kici run remote` works out of the box (see each guide's "run a workflow without pushing" step).
7764
+ - **Both quickstarts wire this up for you** — the [Docker / Podman quickstart](https://docs.kici.dev/user/quickstart/compose/) and the [bare-metal quickstart](https://docs.kici.dev/user/quickstart/bare-metal/) each ship a SeaweedFS object store and pre-fill the orchestrator's `KICI_STORAGE_*` block, so `kici run remote` works out of the box (see each guide's "run a workflow without pushing" step).
7232
7765
  - **A hand-rolled orchestrator deploy does not configure storage by default** — enable a backend before using `kici run remote`:
7233
7766
  - **`filesystem`** — simplest for a single-host orchestrator: set `KICI_STORAGE_TYPE=filesystem` and `KICI_STORAGE_FS_PATH=/var/lib/kici/cache`. No external service needed; blobs are served through the orchestrator's own HMAC-signed HTTP route.
7234
7767
  - **`s3`** — any S3-compatible bucket. **A non-public / self-hosted endpoint works**: set `KICI_STORAGE_TYPE=s3`, `KICI_STORAGE_BUCKET`, `KICI_STORAGE_ENDPOINT=https://your-endpoint` and (for most self-hosted services) `KICI_STORAGE_FORCE_PATH_STYLE=true`. If the developer machine running `kici run remote` reaches the bucket at a different address than the orchestrator, set `KICI_STORAGE_UPLOAD_ENDPOINT` to the developer-reachable address; if agents reach it at yet another address (e.g. agents in containers), set `KICI_STORAGE_EXTERNAL_ENDPOINT` to the agent-routable URL.
7235
7768
 
7236
- See [Storage layout](../operator/orchestrator/storage-layout.md) for the full env-var reference.
7769
+ See [Storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/) for the full env-var reference.
7237
7770
  :::
7238
7771
 
7239
7772
  ## Getting started
@@ -7244,7 +7777,7 @@ See [Storage layout](../operator/orchestrator/storage-layout.md) for the full en
7244
7777
  kici login
7245
7778
  ```
7246
7779
 
7247
- This opens your browser for OAuth authentication and stores a personal access token in `~/.kici/config`. For CI/CD pipelines or headless environments, use `kici login --token <your-api-key>` or `kici login --device` instead. See [CLI authentication](cli-auth.md) for details.
7780
+ This opens your browser for OAuth authentication and stores a personal access token in `~/.kici/config`. For CI/CD pipelines or headless environments, use `kici login --token <your-api-key>` or `kici login --device` instead. See [CLI authentication](https://docs.kici.dev/user/cli-auth/) for details.
7248
7781
 
7249
7782
  ### 2. Write a test fixture
7250
7783
 
@@ -7352,8 +7885,8 @@ This maps the `db` secret context to the `test-database` context, and `api` to `
7352
7885
 
7353
7886
  This mapping is honored by **both** `kici run local` and `kici run remote`:
7354
7887
 
7355
- - For **`kici run local`** (see [`kici run local`](cli-reference.md#kici-run-local)), each named context is resolved from your local secret files (`.kici/.secrets`, `.env.local`, `secrets.yaml`, and `--env` flags).
7356
- - For **`kici run remote`**, each named context maps to an orchestrator **environment**, and the orchestrator resolves that environment's secrets for the run. The target environment must be flagged `allowLocalExecution: true` — mapping a context to a missing or non-test environment rejects the run (see [Secret contexts for testing](#secret-contexts-for-testing) below).
7888
+ - For **`kici run local`** (see [`kici run local`](https://docs.kici.dev/user/cli-reference/#kici-run-local)), each named context is resolved from your local secret files (`.kici/.secrets`, `.env.local`, `secrets.yaml`, and `--env` flags).
7889
+ - For **`kici run remote`**, each named context maps to an orchestrator **environment**, and the orchestrator resolves that environment's secrets for the run. The target environment must be flagged `allowLocalExecution: true` — mapping a context to a missing or non-test environment rejects the run (see [Secret contexts for testing](https://docs.kici.dev/user/testing-guide/#secret-contexts-for-testing) below).
7357
7890
 
7358
7891
  ### Async fixtures
7359
7892
 
@@ -7508,7 +8041,7 @@ Because these values originate on your machine, they are the natural place to pu
7508
8041
 
7509
8042
  In addition to your uploaded values, the orchestrator resolves test-scoped secrets from its own store for a remote test run:
7510
8043
 
7511
- - The job's own declared `environment` contributes its resolved secrets (flat). Static strings and **pure dynamic functions** both participate: a pure `environment:` function (see [Dynamic values](dynamic-values.md)) is evaluated against the fixture's simulated event, and the resolved name is gated and resolved like a static one. Impure dynamic functions (those requiring an init job) are not evaluated for test runs — use a fixture `secrets:` mapping (or `--context`) to supply such a job's secrets.
8044
+ - The job's own declared `environment` contributes its resolved secrets (flat). Static strings and **pure dynamic functions** both participate: a pure `environment:` function (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is evaluated against the fixture's simulated event, and the resolved name is gated and resolved like a static one. Impure dynamic functions (those requiring an init job) are not evaluated for test runs — use a fixture `secrets:` mapping (or `--context`) to supply such a job's secrets.
7512
8045
  - Each fixture `secrets: { ctx: envName }` mapping resolves the named environment's secrets under the namespaced context `ctx`.
7513
8046
 
7514
8047
  Both paths are restricted to environments flagged `allowLocalExecution: true`. A production environment left at the default `false` is never resolvable for a test run.
@@ -7553,7 +8086,7 @@ or via the dashboard's "Test runs" toggle on the environment detail page. `kici
7553
8086
 
7554
8087
  ### Local execution as an alternative
7555
8088
 
7556
- `kici run local` resolves the same local secret files entirely on your machine and honors the fixture `secrets: { ... }` mapping to pick which local context backs each name (see [`kici run local`](cli-reference.md#kici-run-local)). Because the values never leave your machine, it's a good fit when you want to exercise secret-dependent steps without involving the orchestrator at all.
8089
+ `kici run local` resolves the same local secret files entirely on your machine and honors the fixture `secrets: { ... }` mapping to pick which local context backs each name (see [`kici run local`](https://docs.kici.dev/user/cli-reference/#kici-run-local)). Because the values never leave your machine, it's a good fit when you want to exercise secret-dependent steps without involving the orchestrator at all.
7557
8090
 
7558
8091
  ### Discovering available contexts
7559
8092
 
@@ -7630,9 +8163,9 @@ export const pushMain = fixture('push-main', {
7630
8163
 
7631
8164
  ## See also
7632
8165
 
7633
- - [CLI reference](cli-reference.md) -- complete command reference for all `kici` commands
7634
- - [SDK reference](sdk-reference.md) -- trigger functions, step context, and workflow API
7635
- - [Workflow patterns](workflow-patterns.md) -- example workflows to test against
8166
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/) -- complete command reference for all `kici` commands
8167
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) -- trigger functions, step context, and workflow API
8168
+ - [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/) -- example workflows to test against
7636
8169
 
7637
8170
  ---
7638
8171
 
@@ -7640,24 +8173,24 @@ export const pushMain = fixture('push-main', {
7640
8173
 
7641
8174
  Source: https://docs.kici.dev/user/workflow-patterns/
7642
8175
 
7643
- Practical patterns for building real-world KiCI workflows in TypeScript. The patterns are organised across five pages -- start with [Basic CI](./patterns/basic.md) if you're new, or jump to [Integrations](./patterns/integrations.md) if you're wiring up a non-GitHub forge or a generic webhook.
8176
+ Practical patterns for building real-world KiCI workflows in TypeScript. The patterns are organised across five pages -- start with [Basic CI](https://docs.kici.dev/user/patterns/basic/) if you're new, or jump to [Integrations](https://docs.kici.dev/user/patterns/integrations/) if you're wiring up a non-GitHub forge or a generic webhook.
7644
8177
 
7645
8178
  | Page | Covers |
7646
8179
  | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
7647
- | [Basic CI](./patterns/basic.md) | Single-job CI, PR-only / push-only filters, multiple triggers on one workflow, manual-only workflows. |
7648
- | [Conditionals & matrix](./patterns/conditionals-matrix.md) | Conditional execution with rules, matrix builds (static + dynamic), and dynamic job generation. |
7649
- | [Integrations](./patterns/integrations.md) | Workflow chaining, generic webhooks, Stripe handlers, self-hosted git forges (Forgejo / Gitea / Gogs), plain GitHub repo webhooks (no GitHub App). |
7650
- | [Scheduling & events](./patterns/scheduling-and-events.md) | Nightly cron, workflow-complete-triggered deploys, custom event chaining. |
7651
- | [Pattern reference](./patterns/reference.md) | Step context, the examples repository, and GitHub check run output -- cross-cutting reference shared by every pattern above. |
8180
+ | [Basic CI](https://docs.kici.dev/user/patterns/basic/) | Single-job CI, PR-only / push-only filters, multiple triggers on one workflow, manual-only workflows. |
8181
+ | [Conditionals & matrix](https://docs.kici.dev/user/patterns/conditionals-matrix/) | Conditional execution with rules, matrix builds (static + dynamic), and dynamic job generation. |
8182
+ | [Integrations](https://docs.kici.dev/user/patterns/integrations/) | Workflow chaining, generic webhooks, Stripe handlers, self-hosted git forges (Forgejo / Gitea / Gogs), plain GitHub repo webhooks (no GitHub App). |
8183
+ | [Scheduling & events](https://docs.kici.dev/user/patterns/scheduling-and-events/) | Nightly cron, workflow-complete-triggered deploys, custom event chaining. |
8184
+ | [Pattern reference](https://docs.kici.dev/user/patterns/reference/) | Step context, the examples repository, and GitHub check run output -- cross-cutting reference shared by every pattern above. |
7652
8185
 
7653
8186
  ## See also
7654
8187
 
7655
- - [Event system](events.md) -- event model concepts, registration model, circuit breaker
7656
- - [SDK reference](sdk-reference.md) -- complete API reference for all functions used in these patterns
7657
- - [CLI reference](cli-reference.md) -- how to compile and test these workflows locally
7658
- - [Getting started](getting-started.md) -- installation and first workflow setup
7659
- - [Job execution lifecycle](../architecture/execution/job-execution.md) -- how agents execute the jobs defined in these patterns
7660
- - [GitHub checks architecture](../architecture/webhooks/github-checks.md) -- deep dive into the check run system
8188
+ - [Event system](https://docs.kici.dev/user/events/) -- event model concepts, registration model, circuit breaker
8189
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) -- complete API reference for all functions used in these patterns
8190
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/) -- how to compile and test these workflows locally
8191
+ - [Getting started](https://docs.kici.dev/user/getting-started/) -- installation and first workflow setup
8192
+ - [Job execution lifecycle](https://docs.kici.dev/architecture/execution/job-execution/) -- how agents execute the jobs defined in these patterns
8193
+ - [GitHub checks architecture](https://docs.kici.dev/architecture/webhooks/github-checks/) -- deep dive into the check run system
7661
8194
 
7662
8195
  ---
7663
8196
 
@@ -7677,7 +8210,7 @@ your organizations, roles, and API keys stay attached to the same identity.
7677
8210
  Sign-in methods and passwords are managed in your **account console**, provided
7678
8211
  by the identity provider that handles single sign-on for KiCI. The dashboard's
7679
8212
  **Linked accounts** page does not control how you sign in — see
7680
- [Linked accounts vs sign-in methods](#linked-accounts-vs-sign-in-methods) below.
8213
+ [Linked accounts vs sign-in methods](https://docs.kici.dev/user/account-and-login/#linked-accounts-vs-sign-in-methods) below.
7681
8214
 
7682
8215
  You can open the account console from the dashboard: go to your personal
7683
8216
  settings, open **Linked accounts**, and use the **Account console** link.
@@ -7727,9 +8260,9 @@ You declare a gate in your workflow with `approval`. It is available at three le
7727
8260
  - **Job** — hold the job before any of its steps run.
7728
8261
  - **Workflow** — hold the whole run before any job is dispatched.
7729
8262
 
7730
- A step-level gate can also fire **only when a check/apply step finds drift** — Terraform's plan→apply, per step. See [Drift gates](#drift-gates-whendrift) below.
8263
+ A step-level gate can also fire **only when a check/apply step finds drift** — Terraform's plan→apply, per step. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-whendrift) below.
7731
8264
 
7732
- 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.
8265
+ 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)](https://docs.kici.dev/operator/approvals/) for how operators define teams, the approval queue, and expiry; see [the architecture overview](https://docs.kici.dev/architecture/approvals/) for how a hold is evaluated and resumed.
7733
8266
 
7734
8267
  ## Quick start
7735
8268
 
@@ -7750,7 +8283,7 @@ export default workflow('deploy', {
7750
8283
  });
7751
8284
  ```
7752
8285
 
7753
- 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.
8286
+ 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`](https://docs.kici.dev/user/approvals/#approving-from-the-cli) command. Once a member of `leads` approves, the job dispatches normally.
7754
8287
 
7755
8288
  ## The `approval` field
7756
8289
 
@@ -7798,12 +8331,12 @@ approval: {
7798
8331
 
7799
8332
  | Field | Type | Description |
7800
8333
  | ----------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7801
- | `when` | `'always' \| 'drift'` | When the gate fires. `'always'` (default) gates before the element; `'drift'` gates a check/apply step only when it finds drift. See [Drift gates](#drift-gates-whendrift). |
8334
+ | `when` | `'always' \| 'drift'` | When the gate fires. `'always'` (default) gates before the element; `'drift'` gates a check/apply step only when it finds drift. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-whendrift). |
7802
8335
  | `approvers` | `ApproverClause[]` | The AND list of `{ team }` / `{ user }` clauses. An empty list means "any approval-capable member". |
7803
8336
  | `reason` | `string` | A human-readable label shown in the dashboard queue and the held-for-approval status check. |
7804
8337
  | `timeout` | `number` | Per-gate expiry in **seconds**, overriding the org default. On expiry the element is rejected. |
7805
8338
 
7806
- 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).
8339
+ 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](https://docs.kici.dev/operator/approvals/#expiry).
7807
8340
 
7808
8341
  ## Granularity
7809
8342
 
@@ -7855,13 +8388,13 @@ job('migrate-and-deploy', {
7855
8388
 
7856
8389
  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.
7857
8390
 
7858
- 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).
8391
+ 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](https://docs.kici.dev/operator/approvals/#agent-occupancy-during-step-level-holds).
7859
8392
 
7860
8393
  ## Drift gates (`when: 'drift'`)
7861
8394
 
7862
- A `when: 'drift'` gate is **step-scope only** and requires a [check/apply step](idempotent-steps.md). Instead of pausing unconditionally, it fires **between the step's `check` and `run`, only when `check` finds drift in apply mode** — exactly Terraform's plan→apply, scoped to one step. When the step is already in sync (no drift), nothing pauses and the step skips.
8395
+ A `when: 'drift'` gate is **step-scope only** and requires a [check/apply step](https://docs.kici.dev/user/idempotent-steps/). Instead of pausing unconditionally, it fires **between the step's `check` and `run`, only when `check` finds drift in apply mode** — exactly Terraform's plan→apply, scoped to one step. When the step is already in sync (no drift), nothing pauses and the step skips.
7863
8396
 
7864
- When the gate fires, the held run carries the **computed drift** as a payload: the rendering your `summarize(drift)` produced (the per-file diff, the commands that would run), plus the structured drift. The dashboard approval queue and the [CLI](#approving-from-the-cli) show the actual diff the operator is approving — not a static reason string.
8397
+ When the gate fires, the held run carries the **computed drift** as a payload: the rendering your `summarize(drift)` produced (the per-file diff, the commands that would run), plus the structured drift. The dashboard approval queue and the [CLI](https://docs.kici.dev/user/approvals/#approving-from-the-cli) show the actual diff the operator is approving — not a static reason string.
7865
8398
 
7866
8399
  ```typescript
7867
8400
  job('patch-prod', {
@@ -7889,7 +8422,7 @@ A `when: 'drift'` gate on a step without a `check` facet, or at job/workflow sco
7889
8422
 
7890
8423
  ## Mandatory vs. explicit gates
7891
8424
 
7892
- `approval` 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.
8425
+ `approval` 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](https://docs.kici.dev/user/environments/#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.
7893
8426
 
7894
8427
  ## Approving from the CLI
7895
8428
 
@@ -7909,7 +8442,7 @@ kici approve <run-id> --job migrate-and-deploy --step apply-migration
7909
8442
  kici reject <run-id> --job deploy-production --reason "Wrong release branch"
7910
8443
  ```
7911
8444
 
7912
- 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.
8445
+ 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`](https://docs.kici.dev/user/cli-reference/#kici-approve) for the full command reference.
7913
8446
 
7914
8447
  ### Inline approval and `--approve-all` in `kici run remote`
7915
8448
 
@@ -7923,14 +8456,14 @@ kici run remote deploy-prod --approve-all
7923
8456
 
7924
8457
  `--approve-all` is **run-scoped** — it only auto-approves holds belonging to the run this invocation dispatched; there is no fleet-wide or account-wide auto-approve. Eligibility is still enforced per hold: if you are not eligible for a gate, that gate still blocks. Each auto-approved gate prints its payload before resolving and is recorded distinctly in the audit trail (`held_run.auto_approve`).
7925
8458
 
7926
- You can also approve from the dashboard approval queue. See [Dashboard](dashboard/environments-and-secrets.md#approval-queue).
8459
+ You can also approve from the dashboard approval queue. See [Dashboard](https://docs.kici.dev/user/dashboard/environments-and-secrets/#approval-queue).
7927
8460
 
7928
8461
  ## See also
7929
8462
 
7930
- - [Idempotent steps](idempotent-steps.md) — the check/apply step facet that drift gates build on.
7931
- - [Environments](environments.md) — operator-required reviewers on protected environments.
7932
- - [Approval gates (operator guide)](../operator/approvals.md) — teams, the approval queue, expiry, and self-approval.
7933
- - [Approval gates (architecture)](../architecture/approvals.md) — the unified hold model and the step-level round-trip.
8463
+ - [Idempotent steps](https://docs.kici.dev/user/idempotent-steps/) — the check/apply step facet that drift gates build on.
8464
+ - [Environments](https://docs.kici.dev/user/environments/) — operator-required reviewers on protected environments.
8465
+ - [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) — teams, the approval queue, expiry, and self-approval.
8466
+ - [Approval gates (architecture)](https://docs.kici.dev/architecture/approvals/) — the unified hold model and the step-level round-trip.
7934
8467
 
7935
8468
  ---
7936
8469
 
@@ -7990,7 +8523,7 @@ group: () => 'deploy';
7990
8523
  group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
7991
8524
  ```
7992
8525
 
7993
- The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. This differs from job-level `concurrencyGroup` (see [Environments](environments.md#concurrency-groups)), where the compiler performs purity analysis and can inline pure functions for orchestrator-side evaluation.
8526
+ The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. This differs from job-level `concurrencyGroup` (see [Environments](https://docs.kici.dev/user/environments/#concurrency-groups)), where the compiler performs purity analysis and can inline pure functions for orchestrator-side evaluation.
7994
8527
 
7995
8528
  ## cancelInProgress mode
7996
8529
 
@@ -8141,7 +8674,7 @@ Queued runs can be cancelled before they start executing. The cancel request rem
8141
8674
 
8142
8675
  ## Job-level concurrency groups
8143
8676
 
8144
- In addition to workflow-level concurrency, individual jobs can define their own concurrency group via the `concurrencyGroup` property. This controls concurrent execution at the job level rather than the workflow level. See [Environments — concurrency groups](environments.md#concurrency-groups) for details.
8677
+ In addition to workflow-level concurrency, individual jobs can define their own concurrency group via the `concurrencyGroup` property. This controls concurrent execution at the job level rather than the workflow level. See [Environments — concurrency groups](https://docs.kici.dev/user/environments/#concurrency-groups) for details.
8145
8678
 
8146
8679
  ## Local execution
8147
8680
 
@@ -8149,7 +8682,7 @@ In addition to workflow-level concurrency, individual jobs can define their own
8149
8682
 
8150
8683
  Coordination is local only. Running the same workflow on two different machines does not serialize across them — that requires the orchestrator. For full cross-host enforcement (queueing across agents, dashboard visibility, `max > 1`), use `kici run remote` against a deployed orchestrator.
8151
8684
 
8152
- Lock files live under `$XDG_RUNTIME_DIR/kici-local-locks/` on Linux, falling back to `os.tmpdir()/kici-local-locks-<uid>/`. A workflow whose `group` callback throws aborts the run with a clear error rather than running unprotected. See [`kici run local` — Concurrency enforcement](cli-reference.md#concurrency-enforcement) for the `KICI_LOCAL_LOCK_KILL_GRACE_MS` override and the diagnostic output emitted while contending on a busy lock.
8685
+ Lock files live under `$XDG_RUNTIME_DIR/kici-local-locks/` on Linux, falling back to `os.tmpdir()/kici-local-locks-<uid>/`. A workflow whose `group` callback throws aborts the run with a clear error rather than running unprotected. See [`kici run local` — Concurrency enforcement](https://docs.kici.dev/user/cli-reference/#concurrency-enforcement) for the `KICI_LOCAL_LOCK_KILL_GRACE_MS` override and the diagnostic output emitted while contending on a busy lock.
8153
8686
 
8154
8687
  The `kici run local --concurrency <n>` flag is a separate concept — it caps **job-level** parallelism within a single run (how many jobs from one workflow run at once), not cross-run serialization.
8155
8688
 
@@ -8169,14 +8702,14 @@ This guide is split across the following pages:
8169
8702
 
8170
8703
  | Page | Covers |
8171
8704
  | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
8172
- | [Getting started](./dashboard/getting-started.md) | Onboarding checklist and your organizations list |
8173
- | [Navigation and layout](./dashboard/navigation.md) | Sidebar, mobile nav, theme, time display, shortcuts, error pages |
8174
- | [Runs and logs](./dashboard/runs.md) | Run list, run detail, the log viewer |
8175
- | [Settings](./dashboard/settings.md) | Members, roles, teams, keys, sources, billing, security, support access |
8176
- | [Workflows, diagnostics, and orchestrators](./dashboard/workflows-and-diagnostics.md) | Registered workflows, infra health, per-cluster views |
8177
- | [Environments, secrets, and approvals](./dashboard/environments-and-secrets.md) | Environments, secret scopes, approval queue |
8178
- | [Activity and DLQ](./dashboard/activity-and-dlq.md) | Forensic activity log and dead-letter queue |
8179
- | [Account](./dashboard/account.md) | Personal account settings |
8705
+ | [Getting started](https://docs.kici.dev/user/dashboard/getting-started/) | Onboarding checklist and your organizations list |
8706
+ | [Navigation and layout](https://docs.kici.dev/user/dashboard/navigation/) | Sidebar, mobile nav, theme, time display, shortcuts, error pages |
8707
+ | [Runs and logs](https://docs.kici.dev/user/dashboard/runs/) | Run list, run detail, the log viewer |
8708
+ | [Settings](https://docs.kici.dev/user/dashboard/settings/) | Members, roles, teams, keys, sources, billing, security, support access |
8709
+ | [Workflows, diagnostics, and orchestrators](https://docs.kici.dev/user/dashboard/workflows-and-diagnostics/) | Registered workflows, infra health, per-cluster views |
8710
+ | [Environments, secrets, and approvals](https://docs.kici.dev/user/dashboard/environments-and-secrets/) | Environments, secret scopes, approval queue |
8711
+ | [Activity and DLQ](https://docs.kici.dev/user/dashboard/activity-and-dlq/) | Forensic activity log and dead-letter queue |
8712
+ | [Account](https://docs.kici.dev/user/dashboard/account/) | Personal account settings |
8180
8713
 
8181
8714
  ---
8182
8715
 
@@ -8294,7 +8827,7 @@ env: async (event) => {
8294
8827
  - **Write pure functions whenever possible** to avoid the init-job delay. Most environment and env computations only need the event payload data.
8295
8828
  - **Check compiler warnings** -- the compiler tells you when a function is classified as impure and explains why.
8296
8829
  - **Runtime errors in inline expressions cause immediate job failure.** There is no fallback to the init-job path. If your pure function throws at runtime (e.g., accessing a property on `undefined`), the job fails immediately.
8297
- - **The event parameter is the normalized event envelope** — the same shape rules receive as `ctx.event`: `{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }` (see the [event payload reference](./sdk/event-payloads.md) for the complete schema). Narrow on `event.type` (`'push'`, `'pull_request'`, `'tag'`, …) to branch per trigger kind. The raw provider webhook body is nested at `event.payload` (for GitHub pushes: `payload.ref`, `payload.after`, `payload.repository`, …).
8830
+ - **The event parameter is the normalized event envelope** — the same shape rules receive as `ctx.event`: `{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }` (see the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/) for the complete schema). Narrow on `event.type` (`'push'`, `'pull_request'`, `'tag'`, …) to branch per trigger kind. The raw provider webhook body is nested at `event.payload` (for GitHub pushes: `payload.ref`, `payload.after`, `payload.repository`, …).
8298
8831
 
8299
8832
  ---
8300
8833
 
@@ -8461,7 +8994,37 @@ job('deploy-review', {
8461
8994
  });
8462
8995
  ```
8463
8996
 
8464
- A pure function like the one above (see [Dynamic values](dynamic-values.md)) is evaluated inline at dispatch with no init-job overhead. Dynamic environments that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
8997
+ A pure function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is evaluated inline at dispatch with no init-job overhead. Dynamic environments that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
8998
+
8999
+ ### Multiple environments per job
9000
+
9001
+ A job can bind more than one environment with `environments`, an ordered array. This lets a single job draw secrets and variables from several environments at once — for example a shared `staging` environment plus a `my-testing` environment that carries test-only variables:
9002
+
9003
+ ```typescript
9004
+ job('deploy', {
9005
+ runsOn: 'default',
9006
+ environments: ['staging', 'my-testing'],
9007
+ steps: [
9008
+ step('deploy', async (ctx) => {
9009
+ // ctx.secrets and ctx.env carry the merged set from both environments
9010
+ const dbUrl = await ctx.secrets.get('DB_URL');
9011
+ }),
9012
+ ],
9013
+ });
9014
+ ```
9015
+
9016
+ - `environment` (singular) and `environments` (array) are mutually exclusive — setting both is a compile error. `environment: 'staging'` is exactly equivalent to `environments: ['staging']`.
9017
+ - Each array entry is a static name or a function of the event, resolved per element exactly like a single dynamic environment.
9018
+
9019
+ **Merge order — last wins.** All bound environments are resolved on every dispatch (webhook, scheduled, and test runs alike) and merged in array order. When the same secret or variable key is defined in more than one environment, the later entry in the array wins. With `environments: ['staging', 'my-testing']`, a key defined in both resolves to `my-testing`'s value; keys defined in only one are preserved. The longest-scope-path-wins rule still applies _within_ each environment.
9020
+
9021
+ **Protection rules combine all-must-pass.** A job must satisfy **every** bound environment's gates — adding an environment can never loosen access. Branch restrictions, trigger-type filters, and repo patterns must pass for all environments; the minimum trust tier is the most restrictive across them; required reviewers are the union of all environments' reviewers; the wait timer is the longest; and the hold expiry is the shortest. If a run is gated out, the rejection names which environment and which rule rejected it (visible via `kici status` and the run's rejection reason), so a mutually-exclusive set of rules surfaces as a clear failure rather than a silent perpetual rejection.
9022
+
9023
+ **Skip-on-test.** On a test or local run (`kici run remote`, `kici run local`), any bound environment that disallows local execution is skipped — its variables and secrets are omitted from the merge and its gates are not evaluated. This makes the test-only-variables pattern work: with `environments: ['staging', 'my-testing']` where only `my-testing` allows local execution, a test run resolves just `my-testing`'s variables. If every bound environment disallows test runs, the job runs with no environment variables and a clear warning.
9024
+
9025
+ **Unconfigured environments contribute nothing at dispatch.** At dispatch time a bound environment name with no matching configured environment (and no matching glob environment) simply adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic environment resolving to an as-yet-unconfigured name does today.
9026
+
9027
+ **Registration rejects a provably-unsatisfiable binding.** When a workflow is registered, KiCI statically checks every multi-environment binding: a bound environment that does not exist, a disabled one, or two environments with mutually-exclusive fixed branch / trigger-type / repository restrictions (no value can satisfy both) makes the binding provably unsatisfiable, and the registration is rejected with a precise message naming the job, the environments, and the rule — for example `unsatisfiable environment binding: job 'deploy' binds environments [staging, my-testing] with mutually exclusive branch restrictions (no value satisfies all bound environments)`. Bindings whose restrictions use globs are undecidable at registration and fall through to the dispatch-time gate check instead.
8465
9028
 
8466
9029
  ### Job-level environment variables
8467
9030
 
@@ -8485,7 +9048,7 @@ job('deploy', {
8485
9048
 
8486
9049
  ### Concurrency groups
8487
9050
 
8488
- Jobs can define their own concurrency groups to control concurrent execution within an environment. For workflow-level concurrency (which applies to all jobs in a workflow), see [Concurrency groups](concurrency.md).
9051
+ Jobs can define their own concurrency groups to control concurrent execution within an environment. For workflow-level concurrency (which applies to all jobs in a workflow), see [Concurrency groups](https://docs.kici.dev/user/concurrency/).
8489
9052
 
8490
9053
  Control concurrent deployments to the same environment:
8491
9054
 
@@ -8502,7 +9065,7 @@ job('deploy', {
8502
9065
  });
8503
9066
  ```
8504
9067
 
8505
- If no `concurrencyGroup` is specified, the environment name is used as the default concurrency group.
9068
+ If no `concurrencyGroup` is specified, the environment name is used as the default concurrency group. For a job bound to multiple environments, the default is the **first** bound environment's name.
8506
9069
 
8507
9070
  ### Step context
8508
9071
 
@@ -8534,7 +9097,7 @@ When a job targets an environment, variables are merged in this order (last wins
8534
9097
  6. **Job env** -- from the `env` property in the SDK
8535
9098
  7. **`setEnv()` calls** -- runtime modifications within steps
8536
9099
 
8537
- > **Note:** Secrets are NOT part of the environment variable merge. They are delivered to the step context via IPC and accessed through `ctx.secrets`, not through `process.env`. See the [step context](#step-context) section above.
9100
+ > **Note:** Secrets are NOT part of the environment variable merge. They are delivered to the step context via IPC and accessed through `ctx.secrets`, not through `process.env`. See the [step context](https://docs.kici.dev/user/environments/#step-context) section above.
8538
9101
 
8539
9102
  ## Protection rules
8540
9103
 
@@ -8558,9 +9121,9 @@ Require manual approval before a job can proceed:
8558
9121
  Required reviewers: alice, bob
8559
9122
  ```
8560
9123
 
8561
- When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](cli-reference.md#kici-approve) command, or the API. Held runs expire after a configurable timeout.
9124
+ When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](https://docs.kici.dev/user/cli-reference/#kici-approve) command, or the API. Held runs expire after a configurable timeout.
8562
9125
 
8563
- This operator-set rule is the **mandatory** form of an approval gate. Workflow authors can also declare gates in code with `approval` at step, job, or workflow level — see [Approval gates](approvals.md). Both forms use the same held-element mechanism and the same queue.
9126
+ This operator-set rule is the **mandatory** form of an approval gate. Workflow authors can also declare gates in code with `approval` at step, job, or workflow level — see [Approval gates](https://docs.kici.dev/user/approvals/). Both forms use the same held-element mechanism and the same queue.
8564
9127
 
8565
9128
  ### Wait timer
8566
9129
 
@@ -8595,7 +9158,7 @@ Trust tier is determined by the contributor's identity link and CI trust RBAC le
8595
9158
 
8596
9159
  The trust tier also affects which lock file is used for PR-triggered runs: trusted contributors use the PR head lock file, while known and unknown contributors use the base branch lock file. This prevents untrusted workflow modifications from affecting execution.
8597
9160
 
8598
- See the [CI security architecture docs](../architecture/security/ci-security.md) for the full trust resolution flow.
9161
+ See the [CI security architecture docs](https://docs.kici.dev/architecture/security/ci-security/) for the full trust resolution flow.
8599
9162
 
8600
9163
  ### Security approval queue
8601
9164
 
@@ -8629,7 +9192,7 @@ Navigate to **Settings > Environments** in the dashboard. Click **New environmen
8629
9192
  - **Fixed** -- applies to jobs that declare exactly this environment name, like `staging` or `production`
8630
9193
  - **Glob** -- applies to any environment name a job declares that matches the pattern, e.g. `review/*` matches a job with `environment: 'review/PR-123'`
8631
9194
 
8632
- The environments list shows each environment's type, whether test runs may use it (the `allowLocalExecution` flag -- see the [testing guide](./testing-guide.md)), and whether it is enabled.
9195
+ The environments list shows each environment's type, whether test runs may use it (the `allowLocalExecution` flag -- see the [testing guide](https://docs.kici.dev/user/testing-guide/)), and whether it is enabled.
8633
9196
 
8634
9197
  ### Environment detail page
8635
9198
 
@@ -8643,6 +9206,12 @@ Each environment has four tabs:
8643
9206
 
8644
9207
  4. **History** -- view filtered runs targeting this environment.
8645
9208
 
9209
+ ### Bound environments on runs
9210
+
9211
+ A job's bound deployment environments are shown as chips on the run detail page (in the job metadata panel) in the order the job declared them, and the distinct set across a run's jobs appears as compact chips on the run list. For a multi-environment job the chips read left-to-right in merge order — later environments override earlier ones on key collisions. A `(dynamic)` chip marks an environment whose name is computed at runtime; it resolves to the real name once the run starts. A job that binds a single environment shows one chip; a job that binds none shows no chip.
9212
+
9213
+ If a multi-environment binding is gated out, the run's failure banner names which environment and which rule rejected it (the same all-must-pass detail surfaced by `kici status`).
9214
+
8646
9215
  ### Secrets management
8647
9216
 
8648
9217
  Secrets are individual encrypted values organized by scope paths (e.g., `aws/prod`, `databases/postgres`). Scopes are bound to environments via bindings:
@@ -8788,7 +9357,7 @@ genericWebhook({
8788
9357
 
8789
9358
  **Config options:** `source` (required), `events`, `match`, `not`, `auth`, `path`, `description`.
8790
9359
 
8791
- The `source` field MUST match the `--name` that an operator passed to `kici-admin source add generic --name <name>` when the source was created — that string is the source's identifier in the orchestrator. Generic webhook sources must be created by an operator before events can be received; see [Operator guide: event routing](../operator/event-routing.md) for setup instructions.
9360
+ The `source` field MUST match the `--name` that an operator passed to `kici-admin source add generic --name <name>` when the source was created — that string is the source's identifier in the orchestrator. Generic webhook sources must be created by an operator before events can be received; see [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) for setup instructions.
8792
9361
 
8793
9362
  ### Schedule events
8794
9363
 
@@ -8898,7 +9467,7 @@ This lookup is fast because the registration index is held in memory and refresh
8898
9467
 
8899
9468
  ### Cross-source webhook delivery
8900
9469
 
8901
- The catch-all `webhook()` trigger (see [SDK reference: webhook()](sdk/triggers.md#webhook)) participates in this same registration lookup, but with one twist: it fires for matching events arriving via **any** inbound webhook source in the same org, not just the source the workflow's repo is bound to. The orchestrator maintains a `(customerId, eventName)` index over webhook trigger registrations and consults it on every inbound generic webhook.
9470
+ The catch-all `webhook()` trigger (see [SDK reference: webhook()](https://docs.kici.dev/user/sdk/triggers/#webhook)) participates in this same registration lookup, but with one twist: it fires for matching events arriving via **any** inbound webhook source in the same org, not just the source the workflow's repo is bound to. The orchestrator maintains a `(customerId, eventName)` index over webhook trigger registrations and consults it on every inbound generic webhook.
8902
9471
 
8903
9472
  The lookup is structurally org-isolated — a generic webhook delivered to org A can never reach a workflow registered against org B, because foreign-org rows live in a different bucket of the index. When a webhook fires across sources, the runtime clone token, repo URL, and check-status posting all come from the **registration's** source bundle, not the inbound source. The inbound source contributes only the event payload.
8904
9473
 
@@ -9035,15 +9604,15 @@ export default workflow('post-deploy', {
9035
9604
  });
9036
9605
  ```
9037
9606
 
9038
- Custom events are delivered immediately when emitted (mid-workflow, not queued until workflow completion). See the [SDK reference: emitting events](sdk/validation-events.md#emitting-events) section for the full `ctx.emit()` API.
9607
+ Custom events are delivered immediately when emitted (mid-workflow, not queued until workflow completion). See the [SDK reference: emitting events](https://docs.kici.dev/user/sdk/validation-events/#emitting-events) section for the full `ctx.emit()` API.
9039
9608
 
9040
9609
  ## See also
9041
9610
 
9042
- - [SDK reference: event triggers](sdk/triggers.md#event-triggers) -- complete API signatures for all trigger builders
9043
- - [SDK reference: emitting events](sdk/validation-events.md#emitting-events) -- `ctx.emit()` and `defineEvent()` API
9044
- - [Workflow patterns: workflow chaining](patterns/integrations.md#workflow-chaining) -- examples of event-driven workflow chains
9045
- - [Operator guide: event routing](../operator/event-routing.md) -- configuring generic webhook sources, trust relationships, and event routing
9046
- - [Architecture: event system](../architecture/webhooks/event-system.md) -- internal event routing design, registration model, cluster synchronization
9611
+ - [SDK reference: event triggers](https://docs.kici.dev/user/sdk/triggers/#event-triggers) -- complete API signatures for all trigger builders
9612
+ - [SDK reference: emitting events](https://docs.kici.dev/user/sdk/validation-events/#emitting-events) -- `ctx.emit()` and `defineEvent()` API
9613
+ - [Workflow patterns: workflow chaining](https://docs.kici.dev/user/patterns/integrations/#workflow-chaining) -- examples of event-driven workflow chains
9614
+ - [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) -- configuring generic webhook sources, trust relationships, and event routing
9615
+ - [Architecture: event system](https://docs.kici.dev/architecture/webhooks/event-system/) -- internal event routing design, registration model, cluster synchronization
9047
9616
 
9048
9617
  ---
9049
9618
 
@@ -9163,9 +9732,9 @@ Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workf
9163
9732
 
9164
9733
  ## See also
9165
9734
 
9166
- - [Architecture — global workflows](../architecture/global-workflows.md) — dual-query dispatch flow, cross-provider auth, security model, lock-file schema.
9167
- - [Universal-git provider](providers/universal-git.md#global-workflows) — how global workflows interact with `generic:<orgId>:<sourceId>` routing keys.
9168
- - [SDK reference](sdk-reference.md) — the full set of triggers that accept `repos:`.
9735
+ - [Architecture — global workflows](https://docs.kici.dev/architecture/global-workflows/) — dual-query dispatch flow, cross-provider auth, security model, lock-file schema.
9736
+ - [Universal-git provider](https://docs.kici.dev/user/providers/universal-git/#global-workflows) — how global workflows interact with `generic:<orgId>:<sourceId>` routing keys.
9737
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) — the full set of triggers that accept `repos:`.
9169
9738
 
9170
9739
  ---
9171
9740
 
@@ -9292,9 +9861,9 @@ describing what would change. The rendering is read-only.
9292
9861
 
9293
9862
  ## See also
9294
9863
 
9295
- - [Idempotent SDK helpers](./sdk/idempotent.md) — the `idempotent()` / `idempotentStep()` convenience wrappers, which always apply on drift inside a single step (no run-level check mode).
9296
- - [Core SDK reference](./sdk/core.md) — the `step()`, `job()`, and `workflow()` factories the check facet extends.
9297
- - [Lock file and drift](./lock-file-and-drift.md) — how the lock file carries step capability flags.
9864
+ - [Idempotent SDK helpers](https://docs.kici.dev/user/sdk/idempotent/) — the `idempotent()` / `idempotentStep()` convenience wrappers (always apply on drift), plus `checkStep()`, the clean-shape sibling that respects the run-level check mode.
9865
+ - [Core SDK reference](https://docs.kici.dev/user/sdk/core/) — the `step()`, `job()`, and `workflow()` factories the check facet extends.
9866
+ - [Lock file and drift](https://docs.kici.dev/user/lock-file-and-drift/) — how the lock file carries step capability flags.
9298
9867
 
9299
9868
  ---
9300
9869
 
@@ -9347,7 +9916,7 @@ export default workflow('build', {
9347
9916
 
9348
9917
  Per-field rules:
9349
9918
 
9350
- - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](/operator/kici-admin-cli#allow-http-npm--permit-non-https-private-npm-registries)).
9919
+ - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/kici-admin-cli#allow-http-npm--permit-non-https-private-npm-registries)).
9351
9920
  - **`scope`** — Optional. When present, the registry serves only that scope (`@my-org`). When absent, this entry becomes the **default** registry — at most one entry may omit `scope`.
9352
9921
  - **`tokenSecret`** — Mandatory `<environment>:<secret-name>`. The orchestrator looks up the secret in the named environment via the per-environment secret resolver. The bare name **must not** contain a colon.
9353
9922
  - **`alwaysAuth`** — Defaults to `true`. Forces npm to send the token on every request (even GETs), which is what most managed-registry providers require.
@@ -9512,7 +10081,7 @@ registries: [
9512
10081
 
9513
10082
  ## Security model
9514
10083
 
9515
- - **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.
10084
+ - **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](https://docs.kici.dev/user/private-registries/#reviewer-gated-installs) below.
9516
10085
  - **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`.
9517
10086
  - **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.
9518
10087
  - **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
@@ -9548,9 +10117,9 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
9548
10117
 
9549
10118
  ## See also
9550
10119
 
9551
- - [Secrets](secrets.md) — how to seed the `<environment>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
9552
- - [Environments](environments.md) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
9553
- - [Operator: `kici-admin org-settings`](/operator/kici-admin-cli#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
10120
+ - [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<environment>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
10121
+ - [Environments](https://docs.kici.dev/user/environments/) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
10122
+ - [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/kici-admin-cli#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
9554
10123
 
9555
10124
  ---
9556
10125
 
@@ -9666,11 +10235,34 @@ statement's build context must match the token's identity claims (a mismatch is
9666
10235
  a hard failure).
9667
10236
 
9668
10237
  ```bash
9669
- kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-file>
10238
+ kici verify-attestation [artifact] --bundle <path-or-url> [--trust-root <url-or-file>]
9670
10239
  ```
9671
10240
 
9672
- You supply the trusted issuer out-of-band via `--trust-root` the verifier
9673
- never trusts the issuer named inside the token. There are two forms:
10241
+ ### Which trust root do I use?
10242
+
10243
+ The trust root is the **KiCI platform's provenance issuer** — the same hosted
10244
+ KiCI platform you `kici login` against. KiCI attestations are issued by, and
10245
+ verified against, that one issuer; there are no competing "roots" to choose
10246
+ between. So the answer to "shouldn't I just use KiCI as the trust root?" is yes
10247
+ — and that's the **default**: omit `--trust-root` and the verifier checks the
10248
+ bundle against the hosted KiCI platform automatically. You only pass
10249
+ `--trust-root` to verify against a different environment or, more commonly, an
10250
+ offline `{ issuer, jwks }` file for air-gapped checks.
10251
+
10252
+ ### Why you supply it out-of-band
10253
+
10254
+ Given there's a single issuer, why pass it at all instead of letting the
10255
+ verifier read it from the token? Because the issuer named **inside** a token
10256
+ cannot be trusted: a forged bundle could carry a token that names
10257
+ `iss: https://attacker.example` _and_ bundle a key set that "verifies" it,
10258
+ making the whole signature chain circular and self-attesting. The verifier
10259
+ therefore pins to an issuer you supply out-of-band and checks the token against
10260
+ _that_ — the bundle is verified against a key set you trust, not one it shipped
10261
+ with. Naming the trust root is a security requirement, not a multiple-choice
10262
+ question.
10263
+
10264
+ To override the default, supply the trusted issuer via `--trust-root`, in one of
10265
+ two forms:
9674
10266
 
9675
10267
  - **Online — an HTTPS issuer URL.** The verifier fetches
9676
10268
  `<url>/.well-known/openid-configuration`, reads its `issuer` and `jwks_uri`,
@@ -9698,7 +10290,10 @@ when it does not (or on an error such as a missing flag or unreachable trust
9698
10290
  root).
9699
10291
 
9700
10292
  ```bash
9701
- # Verify a bundle against a deployed issuer, digest-checking the artifact:
10293
+ # Default: verify against the hosted KiCI platform (no --trust-root needed):
10294
+ kici verify-attestation ./dist/app.tgz --bundle ./app.tgz.kici.json
10295
+
10296
+ # Override the trust root to verify against a specific issuer:
9702
10297
  kici verify-attestation ./dist/app.tgz \
9703
10298
  --bundle ./app.tgz.kici.json \
9704
10299
  --trust-root https://platform.example/issuer
@@ -9709,7 +10304,7 @@ kici verify-attestation ./dist/app.tgz \
9709
10304
  --trust-root ./kici-trust-root.json
9710
10305
  ```
9711
10306
 
9712
- The full flag reference is in the [CLI reference](./cli-reference.md#kici-verify-attestation).
10307
+ The full flag reference is in the [CLI reference](https://docs.kici.dev/user/cli-reference/#kici-verify-attestation).
9713
10308
 
9714
10309
  ## Viewing attestations in the dashboard
9715
10310
 
@@ -9737,11 +10332,49 @@ attestations shows an empty state.
9737
10332
 
9738
10333
  <!-- /help:run-attestations -->
9739
10334
 
10335
+ ## Browsing attestations across runs
10336
+
10337
+ The **Attestations** page (in the org sidebar) lists every build-provenance
10338
+ attestation your organization has produced — not just one run's. It is the
10339
+ supply-chain audit surface: look up "who built `sha256:…`?" by digest, or browse
10340
+ and filter every attestation across all runs.
10341
+
10342
+ <!-- help:attestations-list#browsing-attestations-across-runs -->
10343
+
10344
+ The **Attestations** page lists every build-provenance attestation your organization has produced.
10345
+
10346
+ - **Search** by artifact digest (exact `sha256:…`) or name to trace a specific artifact.
10347
+ - **Filter** by verification status, repository, workflow, job, or date.
10348
+ - Each row's **status badge** is the verdict KiCI recorded when the attestation was produced (`verified`, `failed`, `unverifiable`, or `pending`).
10349
+
10350
+ Open a row for the parsed provenance statement and a live re-verification.
10351
+
10352
+ <!-- /help:attestations-list -->
10353
+
10354
+ The status badge here is the **server-side verdict**, computed once when the
10355
+ attestation was recorded (verify-at-ingest) — so the list stays fast at any
10356
+ size. `verified` means the signature, build identity, and build context all
10357
+ checked out against the provenance issuer; `failed` means verification ran and
10358
+ the bundle did not pass; `unverifiable` means no verdict could be computed (no
10359
+ provenance issuer configured, or its keys could not be read — not a forgery
10360
+ signal); `pending` means the verdict has not been computed yet.
10361
+
10362
+ Opening a row leads to the **attestation detail page**:
10363
+
10364
+ <!-- help:attestation-detail#browsing-attestations-across-runs -->
10365
+
10366
+ This page shows the parsed provenance for one attestation.
10367
+
10368
+ - **Builder identity, source, and build type** come from the signed SLSA statement.
10369
+ - The **stored badge** is the verdict recorded at build time; **Re-verify** runs the check live in your browser against the current signing keys.
10370
+ - **Download** exports the signed bundle for offline verification with `kici verify-attestation`.
10371
+ <!-- /help:attestation-detail -->
10372
+
9740
10373
  ## See also
9741
10374
 
9742
- - [SDK runtime reference](./sdk/runtime.md) — the `ctx.attestProvenance` and
10375
+ - [SDK runtime reference](https://docs.kici.dev/user/sdk/runtime/) — the `ctx.attestProvenance` and
9743
10376
  `ctx.kici.oidc.token` step APIs in full.
9744
- - [CLI reference](./cli-reference.md#kici-verify-attestation) — every
10377
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/#kici-verify-attestation) — every
9745
10378
  `kici verify-attestation` flag and exit code.
9746
10379
 
9747
10380
  ---
@@ -9754,10 +10387,12 @@ KiCI provides an explicit secrets API that gives workflow steps controlled acces
9754
10387
 
9755
10388
  ## Overview
9756
10389
 
9757
- Secrets are managed per-environment in the orchestrator (see [operator docs](/operator/orchestrator/configuration) for setup). When a job runs with an `environment` binding, the agent receives the secret keys available for that environment but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
10390
+ Secrets are managed per-environment in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration) for setup). When a job runs with an `environment` binding, the agent receives the secret keys available for that environment but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
9758
10391
 
9759
10392
  This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
9760
10393
 
10394
+ A job can bind several environments with `environments: ['staging', 'my-testing']`; the secret keys from all bound environments are merged in array order, with a later environment's value winning on a key collision. See [Multiple environments per job](https://docs.kici.dev/user/environments/#multiple-environments-per-job).
10395
+
9761
10396
  ## Where secret values come from
9762
10397
 
9763
10398
  Secret values are written either through the dashboard or through `kici-admin` running against the orchestrator. The orchestrator operator decides — per organization — which surface accepts secret writes. From the workflow author's perspective, the resolution path at run time is identical either way; the difference is where you (or your ops team) **enter** the value.
@@ -9773,7 +10408,7 @@ Use whichever fits the workflow — most small teams stay on the dashboard; ops
9773
10408
 
9774
10409
  ### When the operator has disabled dashboard writes
9775
10410
 
9776
- The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](/operator/security/dashboard-write-policy). When that flip is on:
10411
+ The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy). When that flip is on:
9777
10412
 
9778
10413
  - The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Clicking them shows a tooltip with the exact `kici-admin secret set` invocation needed.
9779
10414
  - The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
@@ -9810,7 +10445,7 @@ Two cross-cutting flags help every mode:
9810
10445
 
9811
10446
  `kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
9812
10447
 
9813
- A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](/operator/security/dashboard-write-policy#cli-input-modes-for-the-plaintext-path).
10448
+ A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy#cli-input-modes-for-the-plaintext-path).
9814
10449
 
9815
10450
  ## Accessing secrets
9816
10451
 
@@ -9968,7 +10603,7 @@ export default workflow('deploy', {
9968
10603
 
9969
10604
  ### Injecting decrypted sops values into the environment
9970
10605
 
9971
- 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.
10606
+ 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](https://docs.kici.dev/user/sdk/runtime/#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.
9972
10607
 
9973
10608
  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:
9974
10609
 
@@ -10001,9 +10636,9 @@ export default workflow('deploy', {
10001
10636
  });
10002
10637
  ```
10003
10638
 
10004
- 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.
10639
+ 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](https://docs.kici.dev/user/sdk/runtime/#exporting-env-from-shell-commands-kici_env--kici_path) for the full `$KICI_ENV` contract.
10005
10640
 
10006
- 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.
10641
+ 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](https://docs.kici.dev/user/secrets/#canonical-sops-example) above.
10007
10642
 
10008
10643
  ## API reference
10009
10644
 
@@ -10045,7 +10680,7 @@ Note that `get()` is async -- you must `await` the result.
10045
10680
 
10046
10681
  When you run `kici types`, the compiler generates a `.kici/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
10047
10682
 
10048
- See [CLI reference](/user/cli) for the `kici types` command.
10683
+ See [CLI reference](https://docs.kici.dev/user/cli) for the `kici types` command.
10049
10684
 
10050
10685
  ---
10051
10686
 
@@ -10060,11 +10695,11 @@ The **GitHub App** is KiCI's flagship source. A single App:
10060
10695
  1. receives `push`, `pull_request`, and related events from every repo it's installed on,
10061
10696
  2. clones repos with a short-lived installation token (no deploy key to manage),
10062
10697
  3. posts workflow / job / step Check runs back to the pull request (see
10063
- [GitHub checks architecture](../../architecture/webhooks/github-checks.md)).
10698
+ [GitHub checks architecture](https://docs.kici.dev/architecture/webhooks/github-checks/)).
10064
10699
 
10065
10700
  You don't need an App for every scenario — if you only care about `push`
10066
10701
  events, don't want to install an App, or are using a non-GitHub forge,
10067
- use the [universal-git provider](universal-git.md) instead.
10702
+ use the [universal-git provider](https://docs.kici.dev/user/providers/universal-git/) instead.
10068
10703
 
10069
10704
  ## GitHub App vs. `github-repo` preset
10070
10705
 
@@ -10136,9 +10771,9 @@ GitHub App "my-org" is live.
10136
10771
 
10137
10772
  | Flag | Effect |
10138
10773
  | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10139
- | `--name <name>` | The App name you _request_ on GitHub (required). GitHub assigns the final name + slug; the stored, displayed name always comes from GitHub (see [Display name and slug](#display-name-and-slug)). |
10774
+ | `--name <name>` | The App name you _request_ on GitHub (required). GitHub assigns the final name + slug; the stored, displayed name always comes from GitHub (see [Display name and slug](https://docs.kici.dev/user/providers/github/#display-name-and-slug)). |
10140
10775
  | `--github-org <slug>` | Create the App under a GitHub organization instead of your personal account. |
10141
- | `--webhook-url <url>` | **Advanced / self-hosted.** Bake this `https://` URL into the App's webhook verbatim and skip the platform-mode webhook-URL resolution (so it works even where the auto-resolved KiCI Platform URL is unavailable). See [Self-hosted webhook URL](#self-hosted-webhook-url-override). |
10776
+ | `--webhook-url <url>` | **Advanced / self-hosted.** Bake this `https://` URL into the App's webhook verbatim and skip the platform-mode webhook-URL resolution (so it works even where the auto-resolved KiCI Platform URL is unavailable). See [Self-hosted webhook URL](https://docs.kici.dev/user/providers/github/#self-hosted-webhook-url-override). |
10142
10777
  | `--no-browser` | Headless mode: the CLI prints a `kici.dev` URL to open, then reads the setup code you paste back. The page is pure client-side — it only displays the short-lived code, which is useless once the CLI exchanges it. |
10143
10778
 
10144
10779
  The manifest flow always creates a **new** App on GitHub. If a source for
@@ -10201,7 +10836,7 @@ the manifest flow — follow these steps.
10201
10836
 
10202
10837
  The first four rows cover the core flow (clone, trigger matching,
10203
10838
  Check runs). The **Organization -> Members** row is only relevant if
10204
- you use [CI trust tiers](../../architecture/security/ci-security.md)
10839
+ you use [CI trust tiers](https://docs.kici.dev/architecture/security/ci-security/)
10205
10840
  on an org-level install — see the event note below.
10206
10841
 
10207
10842
  6. **Subscribe to events.** At minimum: `push`, `pull_request`,
@@ -10213,7 +10848,7 @@ the manifest flow — follow these steps.
10213
10848
  KiCI caches each pull-request author's repository access level (used
10214
10849
  to decide whether workflow changes take effect immediately or are
10215
10850
  held for approval — see
10216
- [CI security](../../architecture/security/ci-security.md)). These
10851
+ [CI security](https://docs.kici.dev/architecture/security/ci-security/)). These
10217
10852
  events let the orchestrator drop stale cache entries the moment a
10218
10853
  contributor's access changes. They are not required for correctness:
10219
10854
  without them the cache simply ages out on its own 15-minute TTL, so a
@@ -10284,7 +10919,7 @@ kici-admin source get-webhook-secret github:12345 # Fetch the secret (for debug
10284
10919
  ```
10285
10920
 
10286
10921
  For the full CLI reference see the `source` section of the
10287
- [kici-admin CLI reference](../../operator/orchestrator/kici-admin-cli.md).
10922
+ [kici-admin CLI reference](https://docs.kici.dev/operator/orchestrator/kici-admin-cli/).
10288
10923
 
10289
10924
  ## Routing keys
10290
10925
 
@@ -10379,7 +11014,7 @@ kici-admin org-settings global-workflows deny-add 'my-org/contrib/*' \
10379
11014
  Global workflows authored in a GitHub App repo can dispatch against
10380
11015
  events from universal-git sources in the same org, and vice versa,
10381
11016
  with each clone using its own source's credentials. See
10382
- [Global workflows](../../architecture/global-workflows.md) for the
11017
+ [Global workflows](https://docs.kici.dev/architecture/global-workflows/) for the
10383
11018
  policy model and cross-source dispatch contract.
10384
11019
 
10385
11020
  ## Check runs
@@ -10397,7 +11032,7 @@ configuration is required beyond installing the App with the
10397
11032
  `checks: write` permission.
10398
11033
 
10399
11034
  For architecture details see
10400
- [GitHub checks architecture](../../architecture/webhooks/github-checks.md).
11035
+ [GitHub checks architecture](https://docs.kici.dev/architecture/webhooks/github-checks/).
10401
11036
 
10402
11037
  ## Rotation
10403
11038
 
@@ -10480,12 +11115,12 @@ and confirm the App is installed on that repo.
10480
11115
 
10481
11116
  ## See also
10482
11117
 
10483
- - [Universal-git provider](universal-git.md) — for Forgejo / Gitea /
11118
+ - [Universal-git provider](https://docs.kici.dev/user/providers/universal-git/) — for Forgejo / Gitea /
10484
11119
  Gogs / GitLab, and for plain-GitHub repos without an App
10485
- - [GitHub checks architecture](../../architecture/webhooks/github-checks.md)
10486
- - [Global workflows](../../architecture/global-workflows.md)
10487
- - [kici-admin CLI reference](../../operator/orchestrator/kici-admin-cli.md)
10488
- - [Event routing](../../operator/event-routing.md) — operator-level
11120
+ - [GitHub checks architecture](https://docs.kici.dev/architecture/webhooks/github-checks/)
11121
+ - [Global workflows](https://docs.kici.dev/architecture/global-workflows/)
11122
+ - [kici-admin CLI reference](https://docs.kici.dev/operator/orchestrator/kici-admin-cli/)
11123
+ - [Event routing](https://docs.kici.dev/operator/event-routing/) — operator-level
10489
11124
  routing-key mechanics
10490
11125
 
10491
11126
  ---
@@ -10504,8 +11139,8 @@ from GitHub or GitLab; the operator drives runs explicitly with the
10504
11139
  > globally-registered or policy workflows the operator controls — a vendored
10505
11140
  > repo baked into an agent image, a repo synced onto a host out-of-band, a
10506
11141
  > golden internal pipeline. For ordinary per-developer CI driven by pull
10507
- > requests and pushes, use the [GitHub App provider](./github.md) or the
10508
- > [universal-git provider](./universal-git.md) against a real forge.
11142
+ > requests and pushes, use the [GitHub App provider](https://docs.kici.dev/user/providers/github/) or the
11143
+ > [universal-git provider](https://docs.kici.dev/user/providers/universal-git/) against a real forge.
10509
11144
 
10510
11145
  ## Trust caveat (read first)
10511
11146
 
@@ -10600,9 +11235,9 @@ the run is created and then fails, rather than being silently dropped.
10600
11235
 
10601
11236
  ## See also
10602
11237
 
10603
- - [Universal-git provider](./universal-git.md) — for a remote forge (or any
11238
+ - [Universal-git provider](https://docs.kici.dev/user/providers/universal-git/) — for a remote forge (or any
10604
11239
  `http://` git server) when there is no shared filesystem.
10605
- - [GitHub App provider](./github.md) — the flagship source for pull-request
11240
+ - [GitHub App provider](https://docs.kici.dev/user/providers/github/) — the flagship source for pull-request
10606
11241
  CI with Checks.
10607
11242
 
10608
11243
  ---
@@ -10617,7 +11252,7 @@ Gitea, Gogs, GitLab, plain GitHub (without the App), and any custom
10617
11252
  webhook-driven forge you can describe in JSONPath.
10618
11253
 
10619
11254
  > **Want Check runs on pull requests?** Use the [GitHub App
10620
- > provider](github.md) instead — it clones via short-lived installation
11255
+ > provider](https://docs.kici.dev/user/providers/github/) instead — it clones via short-lived installation
10621
11256
  > tokens and drives KiCI's enriched Checks UI out of the box. The
10622
11257
  > universal-git `github-repo` preset is the right fallback when you
10623
11258
  > can't install an App.
@@ -10637,7 +11272,7 @@ that back the GitHub App source also serve universal-git sources.
10637
11272
  > the right choice for the **remote-agent** case — point it at an `http://`
10638
11273
  > git server and the agent clones over the network. When the repo instead
10639
11274
  > lives on the agent's own filesystem (a vendored / operator-curated repo),
10640
- > use a [local `file://` source](./local-file.md) and drive it with the
11275
+ > use a [local `file://` source](https://docs.kici.dev/user/providers/local-file/) and drive it with the
10641
11276
  > `kici-admin` CLI.
10642
11277
 
10643
11278
  ## Which preset do I need?
@@ -10811,7 +11446,7 @@ kici-admin org-settings global-workflows deny-add \
10811
11446
  --source "generic:<orgId>:<sourceId>"
10812
11447
  ```
10813
11448
 
10814
- See [Global workflows](../../architecture/global-workflows.md) for the
11449
+ See [Global workflows](https://docs.kici.dev/architecture/global-workflows/) for the
10815
11450
  policy model (`isWorkflowRepoAllowed` + `isSourceRepoAllowed` +
10816
11451
  `isElevatedAccessAllowed`) and the cross-provider dispatch contract.
10817
11452
 
@@ -10855,13 +11490,381 @@ with the right PEM.
10855
11490
 
10856
11491
  # Architecture overview
10857
11492
 
11493
+ ## Configuration architecture
11494
+
11495
+ Source: https://docs.kici.dev/architecture/configuration/
11496
+
11497
+ This document describes the internal design of the orchestrator's configuration management system. For operator-facing documentation, see [Configuration Reference](https://docs.kici.dev/operator/orchestrator/configuration/) and [Config Management Guide](https://docs.kici.dev/operator/orchestrator/config-management/).
11498
+
11499
+ ## Config type system
11500
+
11501
+ The configuration is modeled as three distinct types that merge into a final application config:
11502
+
11503
+ ### LocalConfig
11504
+
11505
+ Per-orchestrator settings loaded from a YAML file. These are instance-specific and never shared:
11506
+
11507
+ ```typescript
11508
+ interface LocalConfig {
11509
+ database: { url: string };
11510
+ instance?: { id?: string; mode?: 'platform' | 'hybrid' | 'independent' };
11511
+ server?: { port?: number; basePath?: string; logLevel?: string };
11512
+ scaler?: { configPath?: string; configDir?: string };
11513
+ }
11514
+ ```
11515
+
11516
+ **Key property:** Every field except `database.url` is optional. An orchestrator can run entirely from env vars with no YAML file.
11517
+
11518
+ ### SharedConfig
11519
+
11520
+ Shared settings stored in the PostgreSQL `config_versions` table. Defined once, shared across all instances:
11521
+
11522
+ ```typescript
11523
+ interface SharedConfig {
11524
+ platform?: { url?: string; token?: string };
11525
+ storage?: { type?: 's3'; bucket?: string; ... };
11526
+ agentAuth?: 'token' | 'none';
11527
+ agentTokenTtlMs?: number;
11528
+ queue?: { maxDepth?: number; timeoutMs?: number };
11529
+ lockfileCache?: { max?: number; ttlMs?: number };
11530
+ staleDetector?: { scanIntervalMs?: number; ... };
11531
+ secrets?: { key?: string; keyFile?: string; bootstrapAdminToken?: string };
11532
+ pgCustomerSecrets?: boolean;
11533
+ cluster?: { joinToken?: string; raftElectionTimeoutMinMs?: number; ... };
11534
+ // ... tuning fields
11535
+ }
11536
+ ```
11537
+
11538
+ **Key property:** All top-level fields are optional. The DB may store a partial config.
11539
+
11540
+ ### AppConfig
11541
+
11542
+ The merged result type used throughout the codebase. Combines `LocalConfig` + `SharedConfig` with resolved defaults:
11543
+
11544
+ ```typescript
11545
+ interface AppConfig {
11546
+ instanceId: string; // From local config or auto-generated
11547
+ mode: 'platform' | 'hybrid' | 'independent';
11548
+ databaseUrl: string; // Flattened from database.url
11549
+ port: number; // Flattened from server.port
11550
+ basePath: string;
11551
+ platformUrl?: string; // Flattened from platform.url
11552
+ platformToken?: string;
11553
+ agentAuth: 'token' | 'none'; // With defaults applied
11554
+ queueMaxDepth: number; // Flattened from queue.maxDepth
11555
+ cluster: { instanceId: string; credentialFile: string; autoRotateCredentials: boolean; peers: string[]; ... };
11556
+ // ... all other fields with defaults
11557
+ }
11558
+ ```
11559
+
11560
+ **Key property:** `AppConfig` uses flat field names (e.g., `databaseUrl` instead of `database.url`) for backward compatibility with the existing codebase. A `flattenToAppConfig()` function handles the mapping.
11561
+
11562
+ ### How they merge
11563
+
11564
+ ```
11565
+ defaults (getDefaults())
11566
+ |
11567
+ v
11568
+ SharedConfig (from DB) ──deepMerge──> merged layer 1+2
11569
+ |
11570
+ v
11571
+ LocalConfig (from YAML) ──deepMerge──> merged layer 1+2+3
11572
+ |
11573
+ v
11574
+ Env var overrides ──apply──> merged layer 1+2+3+4
11575
+ |
11576
+ v
11577
+ flattenToAppConfig() ──flatten──> flat AppConfig shape
11578
+ |
11579
+ v
11580
+ appConfigSchema.safeParse() ──validate──> typed AppConfig
11581
+ ```
11582
+
11583
+ The `deepMerge` function merges objects recursively, replaces arrays (does not merge item-by-item), and skips `undefined`/`null` source values (they do not override existing values).
11584
+
11585
+ ## Resolution chain
11586
+
11587
+ ### Two-phase design
11588
+
11589
+ ```
11590
+ Phase 1 (local-only):
11591
+ YAML file + KICI_ env vars
11592
+ |
11593
+ v
11594
+ resolveLocalConfig() -> { databaseUrl, instanceId, port, mode }
11595
+ |
11596
+ v
11597
+ Connect to PostgreSQL
11598
+ |
11599
+ v
11600
+ Phase 2 (full merge):
11601
+ defaults -> DB -> YAML -> env
11602
+ |
11603
+ v
11604
+ resolveFullConfig() -> AppConfig
11605
+ ```
11606
+
11607
+ **Why two phases?** The database URL must come from local config (YAML or env var) because we need it to connect to PostgreSQL. But the shared config is stored in PostgreSQL. This circular dependency is broken by resolving local config first (Phase 1), connecting to the DB, then doing the full merge (Phase 2).
11608
+
11609
+ ### Env var processing
11610
+
11611
+ Environment variables are processed in two stages:
11612
+
11613
+ 1. **Direct mappings:** `KICI_DATABASE_URL` -> `database.url`, etc. A lookup table in `env-overlay.ts` maps known env var suffixes to config path arrays.
11614
+
11615
+ 2. **Multi-app GitHub provider:** `KICI_PROVIDERS_GITHUB_<APP_NAME>_<FIELD>` is parsed by stripping the `PROVIDERS_GITHUB_` prefix, finding the field suffix (`APP_ID`, `PRIVATE_KEY`, `WEBHOOK_SECRET`), and deriving the app name from the middle segment. App names are lowercased with underscores converted to hyphens (`MAIN_ORG` -> `main-org`).
11616
+
11617
+ Type coercion is applied based on known field types: numeric fields are parsed as numbers, boolean fields are compared against `"true"`, all others remain strings.
11618
+
11619
+ ## Two-phase bootstrap
11620
+
11621
+ ```
11622
+ ┌─────────────────┐
11623
+ │ Process Start │
11624
+ └────────┬────────┘
11625
+
11626
+
11627
+ ┌─────────────────┐
11628
+ │ Load YAML + │ resolveLocalConfig()
11629
+ │ Env Overrides │ -> databaseUrl, instanceId, port, mode
11630
+ └────────┬────────┘
11631
+
11632
+
11633
+ ┌─────────────────┐
11634
+ │ Connect to │ PostgreSQL
11635
+ │ Database │ Run migrations
11636
+ └────────┬────────┘
11637
+
11638
+
11639
+ ┌─────────────────┐
11640
+ │ Load Shared │ SharedConfigStore.getLatest()
11641
+ │ Config from DB │ -> decrypt -> SharedConfig
11642
+ └────────┬────────┘
11643
+
11644
+
11645
+ ┌─────────────────┐
11646
+ │ Full Merge │ resolveFullConfig(local, db, env)
11647
+ │ + Validate │ -> AppConfig
11648
+ └────────┬────────┘
11649
+
11650
+
11651
+ ┌─────────────────┐
11652
+ │ Start Server │ HTTP, WS, scaler, cluster
11653
+ └─────────────────┘
11654
+ ```
11655
+
11656
+ ## DB schema
11657
+
11658
+ ### config_versions table
11659
+
11660
+ ```sql
11661
+ CREATE TABLE config_versions (
11662
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
11663
+ version SERIAL NOT NULL UNIQUE,
11664
+ config JSONB NOT NULL,
11665
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
11666
+ created_by TEXT NOT NULL,
11667
+ description TEXT,
11668
+ encrypted_paths TEXT[] NOT NULL DEFAULT '{}'
11669
+ );
11670
+
11671
+ CREATE INDEX idx_config_versions_version ON config_versions(version DESC);
11672
+ CREATE INDEX idx_config_versions_created_at ON config_versions(created_at DESC);
11673
+ ```
11674
+
11675
+ **Design choices:**
11676
+
11677
+ - **SERIAL version:** Auto-incrementing integer provides a total ordering of config changes. Simple to compare in heartbeats.
11678
+ - **JSONB config:** Stores the full shared config document. JSONB allows future querying/indexing if needed, though we always read the full document.
11679
+ - **Immutable rows:** Each change creates a new version. Old versions are never modified, providing a full audit trail.
11680
+ - **encrypted_paths:** Array of concrete dot-separated paths (e.g., `platform.token`, `cluster.joinToken`) that contain encrypted values. Stored alongside the config so the system knows exactly which fields to decrypt without runtime path list dependency.
11681
+ - **created_by:** Identifies the source of the change (e.g., `cli:seed`, `api:set`, `api:rollback`).
11682
+
11683
+ ### Versioning strategy
11684
+
11685
+ - Version numbers are auto-incrementing integers managed by PostgreSQL SERIAL
11686
+ - Rollback creates a new version (copy of target) rather than reverting to the old version number
11687
+ - Example: versions 1, 2, 3 exist. Rollback to 1 creates version 4 with the content of version 1
11688
+ - This preserves the audit trail: version 4 records when and why the rollback happened
11689
+
11690
+ ## Encryption
11691
+
11692
+ ### Algorithm
11693
+
11694
+ AES-256-GCM via the existing `secrets/crypto.ts` module:
11695
+
11696
+ - **Key:** 32-byte AES-256 key derived from the master key (`KICI_SECRET_KEY`)
11697
+ - **IV:** Random 12-byte initialization vector per encryption
11698
+ - **Auth tag:** 16-byte GCM authentication tag
11699
+ - **AAD:** `config-field:<path>` (e.g., `config-field:platform.token`) -- binds ciphertext to its specific location
11700
+ - **Wire format:** base64(IV || AuthTag || Ciphertext)
11701
+ - **Key version:** Integer stamp for the master-key generation that sealed the row. Every new row is written under the active generation (hydrated from `MAX(key_version)` at startup). `kici-admin rotate-key` bumps the stamp atomically — the decrypt path accepts the current generation, and during the grace window also the previous one (`KICI_SECRET_KEY_OLD`), so historical rows and rollbacks continue to work seamlessly across rotations.
11702
+
11703
+ ### Sensitive field paths
11704
+
11705
+ The following glob patterns define sensitive fields:
11706
+
11707
+ ```typescript
11708
+ const SENSITIVE_FIELD_PATHS = [
11709
+ 'platform.token',
11710
+ 'secrets.key',
11711
+ 'secrets.bootstrapAdminToken',
11712
+ 'cluster.joinToken',
11713
+ ] as const;
11714
+ ```
11715
+
11716
+ ### Encryption flow
11717
+
11718
+ ```
11719
+ Save:
11720
+ config -> resolveGlobPaths(SENSITIVE_FIELD_PATHS)
11721
+ -> for each concrete path: encrypt(value, key, "config-field:<path>")
11722
+ -> store { encrypted_config, encrypted_paths[] }
11723
+
11724
+ Load:
11725
+ row -> for each path in encrypted_paths: decrypt(value, key, "config-field:<path>")
11726
+ -> SharedConfig
11727
+
11728
+ Export (redacted):
11729
+ row -> decrypt (if master key available) -> replace encrypted_paths values with "***REDACTED***"
11730
+ ```
11731
+
11732
+ ### Rollback optimization
11733
+
11734
+ When rolling back, the target version's encrypted config is copied as-is to the new version. No re-encryption is needed because:
11735
+
11736
+ - The same master key applies (all orchestrators share the same key)
11737
+ - The same AAD applies (paths are identical)
11738
+ - The `encrypted_paths` array is preserved from the target version
11739
+
11740
+ ## Hot-Reload
11741
+
11742
+ ### ConfigReloader design
11743
+
11744
+ The `ConfigReloader` class manages the full reload lifecycle:
11745
+
11746
+ ```mermaid
11747
+ flowchart TD
11748
+ SIGHUP["SIGHUP (signal)"] --> trigger
11749
+ HTTP["HTTP POST /reload"] --> trigger
11750
+ Cluster["Cluster auto-fix"] --> trigger["triggerReload()<br/>500ms debounce"]
11751
+
11752
+ trigger --> execute["executeReload()<br/>boolean mutex"]
11753
+
11754
+ execute --> resolveLocal["resolveLocalConfig()"]
11755
+ execute --> getLatest["getLatest (DB)"]
11756
+ execute --> resolveFull["resolveFullConfig()<br/>(merge + validate)"]
11757
+
11758
+ resolveLocal --> check["Check restart-required fields"]
11759
+ getLatest --> check
11760
+ resolveFull --> check
11761
+
11762
+ check --> swap["Atomic swap<br/>onConfigApplied()"]
11763
+
11764
+ swap --> onProvider["onProviderChange (if changed)"]
11765
+ swap --> onScaler["onScalerReload (always)"]
11766
+ swap --> onPlatform["onPlatformReconnect (if changed)"]
11767
+ ```
11768
+
11769
+ ### Safety guarantees
11770
+
11771
+ - **Mutex:** Boolean flag prevents concurrent reloads. Second reload returns `{ success: false, errors: ["Reload already in progress"] }`.
11772
+ - **Debounce:** Rapid triggers (e.g., multiple SIGHUP signals) are collapsed into a single reload with a 500ms window.
11773
+ - **Validation before swap:** The new config must pass full schema validation. On failure, the old config is preserved and an error is logged.
11774
+ - **Restart-required detection:** Fields like `databaseUrl`, `port`, `instanceId` are compared. If changed, the old values are preserved in the applied config and a warning is logged.
11775
+ - **No crash on failure:** The orchestrator always keeps running with the old config if anything goes wrong during reload.
11776
+
11777
+ ### Subsystem callbacks
11778
+
11779
+ The `ConfigReloader` uses a dependency injection pattern with callbacks for subsystem re-initialization:
11780
+
11781
+ | Callback | When Called | Purpose |
11782
+ | --------------------- | ----------------------------- | -------------------------------------------------------------------------------------------- |
11783
+ | `onProviderChange` | Provider config changed | Reserved callback (providers are now DB-managed via sources table; currently always a no-op) |
11784
+ | `onScalerReload` | Always on successful reload | Reload scaler YAML config |
11785
+ | `onPlatformReconnect` | Platform URL or token changed | Reconnect WS to Platform relay |
11786
+ | `onConfigApplied` | Always on successful reload | Atomic config reference swap, increment local config version |
11787
+
11788
+ ### Prometheus metrics
11789
+
11790
+ | Metric | Type | Labels | Description |
11791
+ | ------------------------------- | ------- | ----------------------------------------------------------------------- | ------------------------------------- |
11792
+ | `kici_orch_config_reload_total` | Counter | `result` (attempted/success/failed), `source` (sighup/http/cluster/cli) | Config reload attempts and outcomes |
11793
+ | `kici_orch_config_version` | Gauge | -- | Current shared config version from DB |
11794
+
11795
+ ## Multi-Provider
11796
+
11797
+ ### ProviderRegistry
11798
+
11799
+ The `ProviderRegistry` maps routing keys to provider bundles. Each routing key (e.g., `github:12345`) is associated with a `ProviderBundle` containing:
11800
+
11801
+ - `WebhookNormalizer` -- normalizes incoming webhooks to a standard format
11802
+ - `LockFileFetcher` -- fetches lock files from the repository
11803
+ - `ChangedFilesFetcher` -- determines which files changed
11804
+ - `CloneTokenProvider` -- generates clone tokens for agents
11805
+ - `RepoUrlBuilder` -- builds clone URLs and raw file URLs
11806
+ - `ContributorResolver` -- resolves contributor permissions for trust-tier gating
11807
+ - `CheckStatusPoster` -- posts check statuses (approval/hold) to the git provider
11808
+
11809
+ Provider registrations are managed via the `sources` database table, not via `SharedConfig`. When the orchestrator connects to the Platform relay, it reads source records from the DB and sends `source.register` messages. Changes to sources (add/remove) are detected via PostgreSQL LISTEN/NOTIFY on the `sources_change` channel and pushed to the Platform via `source.secrets` and `source.register`/`source.deregister`.
11810
+
11811
+ ### Per-App Credentials
11812
+
11813
+ Each source record contains its own `appId` and `privateKey` (stored as scoped secrets). When processing a webhook, the orchestrator looks up the routing key to find the matching source and uses its credentials for JWT generation, clone tokens, and check run updates.
11814
+
11815
+ ## Cluster sync
11816
+
11817
+ ### Heartbeat config version
11818
+
11819
+ In clustered deployments, each orchestrator includes its current config version in Raft heartbeat metadata via the `configVersion` optional field on the `peerHeartbeatSchema`.
11820
+
11821
+ When the `PeerRegistry` processes a heartbeat:
11822
+
11823
+ 1. Compare `localConfigVersion` with `peer.configVersion`
11824
+ 2. If `peer.configVersion > localConfigVersion` AND both are > 0:
11825
+ - Invoke the `onConfigVersionBehind` callback
11826
+ - This triggers a config reload from the database
11827
+
11828
+ ### Auto-remediation flow
11829
+
11830
+ ```
11831
+ Orchestrator A (version 5) Orchestrator B (version 3)
11832
+ │ │
11833
+ │──── heartbeat(configVersion=5) ────>│
11834
+ │ │
11835
+ │ compare: 5 > 3
11836
+ │ trigger reload from DB
11837
+ │ │
11838
+ │ resolveFullConfig()
11839
+ │ -> version 5
11840
+ │ │
11841
+ │<── heartbeat(configVersion=5) ──────│
11842
+ │ │
11843
+ │ both at version 5 ✓ │
11844
+ ```
11845
+
11846
+ ### Guard conditions
11847
+
11848
+ - Version comparison only triggers when **both** local and peer versions are > 0
11849
+ - This prevents false triggers from:
11850
+ - Legacy orchestrators that do not report `configVersion` (field is optional, defaults to 0)
11851
+ - Newly started orchestrators before their first config load
11852
+ - The `localConfigVersion` is a monotonically incrementing local counter (incremented on each successful reload)
11853
+
11854
+ ## See also
11855
+
11856
+ - [Configuration Reference](https://docs.kici.dev/operator/orchestrator/configuration/) -- operator guide
11857
+ - [Config Management Guide](https://docs.kici.dev/operator/orchestrator/config-management/) -- CLI and API guide
11858
+
11859
+ ---
11860
+
10858
11861
  ## Data flows
10859
11862
 
10860
11863
  Source: https://docs.kici.dev/architecture/data-flows/
10861
11864
 
10862
11865
  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.
10863
11866
 
10864
- > **Lock file schema version:** The lock file uses schema version 21, which adds the `CheckMode` / `CheckStepOutcome` enums for check-mode step execution on top of v20's `LabelMatcher` (exact/regex) selectors for `runsOn`/`runsOnAll`/`excludeLabels`, v19's `maxParallel`/`failFast` fan-out concurrency, v18's `runsOnAll` host fan-out predicate and `onUnreachable` policy, v17's typed init presets (`mise` / `{ mise }`) and `auto` detection, v16's normalized approval config, v15's per-job init config, 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.
11867
+ > **Lock file schema version:** The lock file uses schema version 29. The orchestrator rejects any fetched lock whose `schemaVersion` does not exactly match the engine version it was built against, so a stale lock must be recompiled with `kici compile` and pushed again after any SDK upgrade that bumps the schema.
10865
11868
 
10866
11869
  ## Webhook delivery flow
10867
11870
 
@@ -10892,7 +11895,7 @@ GitHub --> Platform Relay --> Orchestrator --> Agent
10892
11895
  15. **Orchestrator matches triggers** against lock file using `matchAllWorkflows()` from `@kici-dev/engine`.
10893
11896
  16. **Orchestrator checks caches** for source tarballs and dependency tarballs.
10894
11897
  17. **Orchestrator dispatches jobs** to agents via the job queue and WebSocket.
10895
- 18. **Orchestrator persists a delivery row** keyed by `(org_id, delivery_id)` to its own `event_log`, including a pointer to the gzipped payload in object storage. The orchestrator's delivery log is surfaced in the dashboard's Settings → Event log tab. See [`webhook-delivery.md`](./webhooks/webhook-delivery.md#delivery-log).
11898
+ 18. **Orchestrator persists a delivery row** keyed by `(org_id, delivery_id)` to its own `event_log`, including a pointer to the gzipped payload in object storage. The orchestrator's delivery log is surfaced in the dashboard's Settings → Event log tab. See [`webhook-delivery.md`](https://docs.kici.dev/architecture/webhooks/webhook-delivery/#delivery-log).
10896
11899
 
10897
11900
  ## Job execution flow
10898
11901
 
@@ -10920,7 +11923,7 @@ Orchestrator Agent Sandbox (child pro
10920
11923
 
10921
11924
  ### Agent pipeline
10922
11925
 
10923
- The agent delegates job execution to an `ExecutionSandbox` (container, bare-metal, or firecracker). The sandbox runs customer code in an isolated child process -- never in the agent's V8 isolate. Four job types are handled: execution jobs (sandbox), build-only jobs (in-process, cache population), init-only jobs (in-process, dynamic field resolution), and DynamicJobFn evaluation jobs (in-process, runtime job generation). See [Job execution lifecycle](./execution/job-execution.md) for details.
11926
+ The agent delegates job execution to an `ExecutionSandbox` (container, bare-metal, or firecracker). The sandbox runs customer code in an isolated child process -- never in the agent's V8 isolate. Four job types are handled: execution jobs (sandbox), build-only jobs (in-process, cache population), init-only jobs (in-process, dynamic field resolution), and DynamicJobFn evaluation jobs (in-process, runtime job generation). See [Job execution lifecycle](https://docs.kici.dev/architecture/execution/job-execution/) for details.
10924
11927
 
10925
11928
  1. **Report running** -- Send `job.status: running` immediately upon accepting the dispatch
10926
11929
  2. **Sandbox selection** -- Determine execution mode (container, bare-metal, firecracker) from job config and environment
@@ -10958,7 +11961,7 @@ Run initiation (`upload-init`), the trigger, status polling, log retrieval, and
10958
11961
 
10959
11962
  ### Data plane
10960
11963
 
10961
- 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.
11964
+ 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](https://docs.kici.dev/operator/orchestrator/storage-layout/) for the upload-endpoint configuration.
10962
11965
 
10963
11966
  ### Org anchor
10964
11967
 
@@ -11067,7 +12070,7 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
11067
12070
 
11068
12071
  ### Cross-source / no-contentHash workflows
11069
12072
 
11070
- - **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 21.
12073
+ - **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 29.
11071
12074
  - **Cross-source / global-workflow dispatch** (a workflow registered against source A fired by a webhook on source B) bypasses both caches. The registration's lock file entry still carries `contentHash`, but the cross-source path always clone-and-installs — the eval temp dir doesn't ship `@kici-dev/sdk`. The execution agent still verifies `contentHash` against the cloned source for drift detection.
11072
12075
 
11073
12076
  ### Build deduplication
@@ -11081,7 +12084,7 @@ If cache storage is unavailable or a download fails:
11081
12084
  - **Source tarball download failure:** Hard failure today — the agent does not fall back to `git clone` on the execution path. (The build path is where cloning happens.) In practice this is rare because the same orchestrator that issued the pre-signed URL controls the cache backend.
11082
12085
  - **Dep tarball download failure:** Agent falls back to running `npm ci` / `npm install` inline.
11083
12086
  - **Dep tarball hash mismatch:** Agent retries the download twice (3 total attempts), then fails the job (no fallback for integrity failures).
11084
- - **Source tarball drift (extracted `contentHash` ≠ lock file):** Hard failure with "Lock file is out of date: workflow source changed without regenerating kici.lock.json" — see [Lock file and drift](../user/lock-file-and-drift.md).
12087
+ - **Source tarball drift (extracted `contentHash` ≠ lock file):** Hard failure with "Lock file is out of date: workflow source changed without regenerating kici.lock.json" — see [Lock file and drift](https://docs.kici.dev/user/lock-file-and-drift/).
11085
12088
  - **Build failure:** Execution is skipped entirely with a "Build failed" check status. Workflows that contain dynamic job entries (DynamicJobFn) are allowed to proceed with their dynamic eval jobs since those compile from source.
11086
12089
  - **No cache configured:** Agent runs inline install for every job (pre-caching behavior).
11087
12090
 
@@ -11113,9 +12116,9 @@ The orchestrator derives the target platform/arch for dep cache lookups by probi
11113
12116
 
11114
12117
  ### TTL and eviction (touch-on-read)
11115
12118
 
11116
- Both caches refresh TTL on read via `touch-on-read`. An entry's lifetime is reset every time an orchestrator issues a pre-signed GET URL for it. Default TTL is `KICI_CACHE_TTL_DAYS=30`; entries unused for 30 days expire at the storage level. Actively used sources and deps stay in cache indefinitely as long as they continue to be referenced by inbound webhooks or reruns. See [`docs/operator/dependency-caching.md`](../operator/dependency-caching.md#cache-behavior) for configuration.
12119
+ Both caches refresh TTL on read via `touch-on-read`. An entry's lifetime is reset every time an orchestrator issues a pre-signed GET URL for it. Default TTL is `KICI_CACHE_TTL_DAYS=30`; entries unused for 30 days expire at the storage level. Actively used sources and deps stay in cache indefinitely as long as they continue to be referenced by inbound webhooks or reruns. See [`docs/operator/dependency-caching.md`](https://docs.kici.dev/operator/dependency-caching/#cache-behavior) for configuration.
11117
12120
 
11118
- For the full per-package bucket and prefix inventory — cache, logs, cold-store, and the observability sidecar buckets — see [orchestrator storage layout](../operator/orchestrator/storage-layout.md).
12121
+ For the full per-package bucket and prefix inventory — cache, logs, cold-store, and the observability sidecar buckets — see [orchestrator storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/).
11119
12122
 
11120
12123
  ### Pre-signed URL upload flow
11121
12124
 
@@ -11153,7 +12156,7 @@ Agents receive pre-signed S3 GET URLs (15-minute expiry) directly in `job.dispat
11153
12156
 
11154
12157
  ## User-facing cache flow
11155
12158
 
11156
- The source/dep cache above is internal: the orchestrator owns its keys and decides when to hit or build. The **user-facing cache** is driven by the workflow author — the declarative `cache: { key, paths, restoreKeys? }` on a job/step, or the imperative `ctx.cache.restore()` / `ctx.cache.save()` API (see [SDK caching reference](../user/sdk/caching.md)). It reuses the same object-storage backend and the same direct-to-storage presigned-URL transport, but the agent — not the orchestrator — initiates each restore and save over WebSocket.
12159
+ The source/dep cache above is internal: the orchestrator owns its keys and decides when to hit or build. The **user-facing cache** is driven by the workflow author — the declarative `cache: { key, paths, restoreKeys? }` on a job/step, or the imperative `ctx.cache.restore()` / `ctx.cache.save()` API (see [SDK caching reference](https://docs.kici.dev/user/sdk/caching/)). It reuses the same object-storage backend and the same direct-to-storage presigned-URL transport, but the agent — not the orchestrator — initiates each restore and save over WebSocket.
11157
12160
 
11158
12161
  The agent's cache module archives `paths` into a gzipped tarball (computing a SHA-256 over the bytes) and streams downloads back through a checksum-verified extract pipeline. The orchestrator's `UserCache` owns the `cache/<orgId>/<repoId>/<scope>/<key>` namespacing, the immutable first-save check, the `restoreKeys` prefix scan, the two-phase atomic save, and per-org quota/TTL eviction.
11159
12162
 
@@ -11207,7 +12210,7 @@ The save is **immutable** and **atomic**. The orchestrator declines (`skip: true
11207
12210
 
11208
12211
  ### Trust → scope mapping
11209
12212
 
11210
- The orchestrator threads a `cacheRefScope` onto each `job.dispatch`. A **trusted** ref (the repo's own branches, default branch) maps to the `shared` write scope; any other ref (a fork PR) maps to `isolated`, writing to a per-run `iso/<runId>/` scope. This is the cache-isolation model: a fork can restore from the trusted `shared/` cache but can never write into it, so it cannot poison the entries a trusted branch later restores. The org segment of the key namespace (`cache/<orgId>/`) is the per-tenant boundary — no tenant can read another tenant's cache. See [orchestrator storage layout](../operator/orchestrator/storage-layout.md#user-cache) for the full prefix map and quota/TTL knobs.
12213
+ The orchestrator threads a `cacheRefScope` onto each `job.dispatch`. A **trusted** ref (the repo's own branches, default branch) maps to the `shared` write scope; any other ref (a fork PR) maps to `isolated`, writing to a per-run `iso/<runId>/` scope. This is the cache-isolation model: a fork can restore from the trusted `shared/` cache but can never write into it, so it cannot poison the entries a trusted branch later restores. The org segment of the key namespace (`cache/<orgId>/`) is the per-tenant boundary — no tenant can read another tenant's cache. See [orchestrator storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/#user-cache) for the full prefix map and quota/TTL knobs.
11211
12214
 
11212
12215
  ## Internal event routing flow
11213
12216
 
@@ -11635,7 +12638,7 @@ The compiler processes the workflow definition:
11635
12638
 
11636
12639
  ### Execution time (local test runner)
11637
12640
 
11638
- When `kici test` runs a workflow:
12641
+ When `kici run local` runs a workflow:
11639
12642
 
11640
12643
  1. **SDK module resolution:** The runner resolves `setStepOutputsMap` / `setJobOutputsMap` from the same `@kici-dev/sdk` module instance that the workflow uses (ensures the proxy reads from the same map)
11641
12644
  2. **Map injection:** Fresh `OutputsMap` and `StepRefMap` are created and injected via `setStepOutputsMap()` / `setStepRefMap()` before each job
@@ -11701,15 +12704,15 @@ The Platform tier exposes a `/ws/browser` WebSocket endpoint for dashboard clien
11701
12704
 
11702
12705
  ## See also
11703
12706
 
11704
- - [Architecture overview](overview.md) -- three-tier model and component responsibilities
11705
- - [Protocol messages](protocol-messages.md) -- WebSocket message schemas
11706
- - [Event system internals](./webhooks/event-system.md) -- event router, registration model, cron scheduler
11707
- - [State machine](./execution/state-machine.md) -- job execution state transitions
11708
- - [Webhook delivery](./webhooks/webhook-delivery.md) -- detailed webhook processing pipeline
11709
- - [Operator: dependency caching](../operator/dependency-caching.md) -- configuration guide
11710
- - [Operator: monitoring & tracing](../operator/observability/monitoring.md) -- trace fields and Loki queries
11711
- - [Operator: event routing & generic webhooks](../operator/event-routing.md) -- generic source setup and trust management
11712
- - [SDK reference: output chaining](../user/sdk/core.md#output-chaining) -- user-facing output chaining API
12707
+ - [Architecture overview](https://docs.kici.dev/architecture/overview/) -- three-tier model and component responsibilities
12708
+ - [Protocol messages](https://docs.kici.dev/architecture/protocol-messages/) -- WebSocket message schemas
12709
+ - [Event system internals](https://docs.kici.dev/architecture/webhooks/event-system/) -- event router, registration model, cron scheduler
12710
+ - [State machine](https://docs.kici.dev/architecture/execution/state-machine/) -- job execution state transitions
12711
+ - [Webhook delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/) -- detailed webhook processing pipeline
12712
+ - [Operator: dependency caching](https://docs.kici.dev/operator/dependency-caching/) -- configuration guide
12713
+ - [Operator: monitoring & tracing](https://docs.kici.dev/operator/observability/monitoring/) -- trace fields and Loki queries
12714
+ - [Operator: event routing & generic webhooks](https://docs.kici.dev/operator/event-routing/) -- generic source setup and trust management
12715
+ - [SDK reference: output chaining](https://docs.kici.dev/user/sdk/core/#output-chaining) -- user-facing output chaining API
11713
12716
 
11714
12717
  ---
11715
12718
 
@@ -11764,7 +12767,7 @@ The orchestrator is the execution brain. It decides what to run and dispatches w
11764
12767
  - **Agent registry** -- Tracks connected agents with label-based routing for job dispatch.
11765
12768
  - **Job queue** -- PostgreSQL-backed FIFO queue for reliable dispatch.
11766
12769
  - **Webhook pipeline** -- Dedup, event mapping, lock file fetch, trigger matching, and job dispatch in a single pipeline.
11767
- - **Multi-orchestrator clustering** -- Optional peer-to-peer coordination via direct WebSocket connections. Enables cross-architecture job routing (e.g., x64 coordinator reroutes arm64 jobs to a peer), high availability, and dedicated coordinator topologies. Uses Raft consensus for leader election (orphan recovery). See [Multi-Orchestrator Architecture](./clustering/multi-orchestrator.md).
12770
+ - **Multi-orchestrator clustering** -- Optional peer-to-peer coordination via direct WebSocket connections. Enables cross-architecture job routing (e.g., x64 coordinator reroutes arm64 jobs to a peer), high availability, and dedicated coordinator topologies. Uses Raft consensus for leader election (orphan recovery). See [Multi-Orchestrator Architecture](https://docs.kici.dev/architecture/clustering/multi-orchestrator/).
11768
12771
  - **Auto-scaler** -- Optional pluggable module for ephemeral agent provisioning. Supports containers (Docker/Podman), bare-metal processes, and Firecracker microVMs as backends. Spawns agents on demand when no matching agent is connected, with label-based routing, two-level capacity limits (global + per-backend), warm pools, YAML configuration (`scalers.d/` directory support), and SIGHUP reload. Disabled by default -- orchestrator works without it.
11769
12772
  - **Independent database** -- Has its own PostgreSQL database separate from the Platform. Stores execution runs/jobs/steps, dispatch queue, webhook secrets, dedup cache, and scaler state. The orchestrator's `execution_runs` and `execution_jobs` are the authoritative source of truth. The Platform receives execution status updates via WebSocket messages (`execution.status`, `job.status.forward`).
11770
12773
 
@@ -11903,13 +12906,13 @@ The orchestrator connects outbound to the Platform WebSocket endpoint. After aut
11903
12906
 
11904
12907
  When multiple orchestrators are deployed, they establish direct WebSocket connections to each other on the `/ws/peer` endpoint. Peers are discovered via the Platform matchmaker (Platform/hybrid modes) or static configuration (`KICI_CLUSTER_PEERS` env var, independent mode). Connections are authenticated with a mutual pre-shared key (PSK). Traffic includes agent inventory heartbeats, job rerouting, progress reporting, cancel propagation, and Raft leader election. These messages never transit the Platform tier.
11905
12908
 
11906
- > See [Multi-Orchestrator Architecture](./clustering/multi-orchestrator.md) for clustering details and [Protocol Messages](protocol/dashboard.md#orchestrator-orchestrator-messages-peer-to-peer) for message schemas.
12909
+ > See [Multi-Orchestrator Architecture](https://docs.kici.dev/architecture/clustering/multi-orchestrator/) for clustering details and [Protocol Messages](https://docs.kici.dev/architecture/protocol/dashboard/#orchestrator-orchestrator-messages-peer-to-peer) for message schemas.
11907
12910
 
11908
12911
  ### Orchestrator ↔ Agent
11909
12912
 
11910
12913
  The agent connects outbound to the orchestrator WebSocket endpoint. After registration (agent ID, labels, concurrency), the connection is used for job dispatch, status reporting, and log streaming.
11911
12914
 
11912
- > See [Protocol Messages](protocol-messages.md) and [Webhook Delivery](./webhooks/webhook-delivery.md) for detailed message flows and schemas.
12915
+ > See [Protocol Messages](https://docs.kici.dev/architecture/protocol-messages/) and [Webhook Delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/) for detailed message flows and schemas.
11913
12916
 
11914
12917
  ## Authentication and multi-tenancy
11915
12918
 
@@ -11917,9 +12920,9 @@ KiCI uses application-level tenant isolation. The Platform dashboard API accepts
11917
12920
 
11918
12921
  ## See also
11919
12922
 
11920
- - [Multi-Orchestrator Architecture](./clustering/multi-orchestrator.md) -- P2P clustering, Raft consensus, job rerouting
11921
- - [State Machine](./execution/state-machine.md) -- execution lifecycle tracking across all tiers
11922
- - [Protocol Messages](protocol-messages.md) -- WebSocket message schemas for all three layers
11923
- - [Webhook Delivery](./webhooks/webhook-delivery.md) -- end-to-end trace of a webhook through all three tiers
12923
+ - [Multi-Orchestrator Architecture](https://docs.kici.dev/architecture/clustering/multi-orchestrator/) -- P2P clustering, Raft consensus, job rerouting
12924
+ - [State Machine](https://docs.kici.dev/architecture/execution/state-machine/) -- execution lifecycle tracking across all tiers
12925
+ - [Protocol Messages](https://docs.kici.dev/architecture/protocol-messages/) -- WebSocket message schemas for all three layers
12926
+ - [Webhook Delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/) -- end-to-end trace of a webhook through all three tiers
11924
12927
 
11925
12928
  ---