@kici-dev/compiler 0.1.13 → 0.1.15

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 (70) hide show
  1. package/README.md +21 -1
  2. package/dist/cli.js +27 -22
  3. package/dist/commands/cancel.js +1 -1
  4. package/dist/commands/compile.js +1 -1
  5. package/dist/commands/docs.js +1 -1
  6. package/dist/commands/drain-worker.js +1 -1
  7. package/dist/commands/endpoints.js +1 -1
  8. package/dist/commands/fixture.js +3 -3
  9. package/dist/commands/hook.js +1 -1
  10. package/dist/commands/index.js +6 -6
  11. package/dist/commands/init.js +9 -7
  12. package/dist/commands/login.js +5 -16
  13. package/dist/commands/run.js +19 -8
  14. package/dist/commands/secrets-list.js +2 -2
  15. package/dist/commands/status.js +4 -4
  16. package/dist/commands/test.d.ts +2 -0
  17. package/dist/commands/test.js +2 -2
  18. package/dist/commands/types.js +1 -1
  19. package/dist/commands/watch.js +1 -1
  20. package/dist/commands/workflows.js +3 -3
  21. package/dist/execution/executor.js +6 -3
  22. package/dist/execution/sdk-alias.js +1 -1
  23. package/dist/execution/ts-loader.d.ts +2 -0
  24. package/dist/execution/ts-loader.js +13 -0
  25. package/dist/fixtures/compiler.d.ts +7 -5
  26. package/dist/fixtures/compiler.js +10 -6
  27. package/dist/llm-context/llms-full.txt +1077 -387
  28. package/dist/llm-context/llms.txt +38 -37
  29. package/dist/local-executor/index.js +6 -5
  30. package/dist/local-executor/job-runner.js +6 -5
  31. package/dist/local-executor/materializer.d.ts +5 -0
  32. package/dist/local-executor/materializer.js +22 -1
  33. package/dist/local-executor/output-streamer.js +1 -1
  34. package/dist/local-executor/picker.js +1 -1
  35. package/dist/local-executor/to-event-payload.d.ts +16 -0
  36. package/dist/local-executor/to-event-payload.js +21 -0
  37. package/dist/local-executor/workflow-lock.d.ts +4 -3
  38. package/dist/local-executor/workflow-lock.js +0 -0
  39. package/dist/lockfile/generator.js +11 -6
  40. package/dist/lockfile/hasher.js +1 -1
  41. package/dist/remote/client.d.ts +4 -0
  42. package/dist/remote/client.js +1 -1
  43. package/dist/remote/encryption.d.ts +1 -1
  44. package/dist/remote/encryption.js +1 -1
  45. package/dist/remote/history.js +2 -2
  46. package/dist/remote/oauth.js +1 -1
  47. package/dist/remote/oidc-discovery.js +1 -1
  48. package/dist/remote/output/streaming.js +1 -1
  49. package/dist/remote/output/summary.js +1 -1
  50. package/dist/remote/prod-defaults.d.ts +18 -0
  51. package/dist/remote/prod-defaults.js +23 -0
  52. package/dist/remote/secret-upload.d.ts +20 -0
  53. package/dist/remote/secret-upload.js +58 -0
  54. package/dist/remote/uploader.js +1 -1
  55. package/dist/templates/index.js +1 -1
  56. package/dist/templates/package-json.js +1 -1
  57. package/dist/templates/workflows/pr-checks.js +3 -2
  58. package/dist/templates/workflows/pr-checks.ts +4 -2
  59. package/dist/test-runner/dry-run.js +1 -1
  60. package/dist/test-runner/index.js +4 -4
  61. package/dist/test-runner/job-executor.js +1 -1
  62. package/dist/test-runner/output-formatter.js +1 -1
  63. package/dist/test-runner/payload-builder.js +2 -2
  64. package/dist/test-runner/rule-evaluator.js +1 -1
  65. package/dist/test-runner/step-context.js +6 -2
  66. package/dist/types.d.ts +17 -1
  67. package/dist/types.js +1 -0
  68. package/dist/workflows/pr-checks.ts +4 -2
  69. package/package.json +13 -9
  70. package/sbom.spdx.json +1328 -8436
@@ -6,13 +6,13 @@ This file is the concatenated markdown of every KiCI documentation page intended
6
6
 
7
7
  ## User guide
8
8
 
9
- Source: https://kici.dev/docs/user/
9
+ Source: https://docs.kici.dev/user/
10
10
 
11
11
  Documentation for workflow authors -- people writing CI/CD pipelines in TypeScript using the KiCI SDK and compiler. If you are defining workflows, running local tests, or learning the SDK API, start here.
12
12
 
13
13
  ## Pages
14
14
 
15
- ### [Getting started](getting-started.md)
15
+ ### [Getting started with KiCI](getting-started.md)
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
 
@@ -88,7 +88,7 @@ Cross-repo workflows that let a single workflow repo define jobs which run on ev
88
88
 
89
89
  ## Getting started with KiCI
90
90
 
91
- Source: https://kici.dev/docs/user/getting-started/
91
+ Source: https://docs.kici.dev/user/getting-started/
92
92
 
93
93
  KiCI lets you define CI/CD workflows in TypeScript instead of YAML. You get full language power -- type safety, autocompletion, loops, conditionals, and async/await -- for your build pipelines.
94
94
 
@@ -179,7 +179,7 @@ Create `.kici/workflows/ci.ts`:
179
179
  import { workflow, job, step, pr } from '@kici-dev/sdk';
180
180
 
181
181
  const lint = job('lint', {
182
- runsOn: 'ubuntu-latest',
182
+ runsOn: 'linux',
183
183
  steps: [
184
184
  step('install', async ({ $ }) => {
185
185
  await $`pnpm install --frozen-lockfile`;
@@ -191,7 +191,7 @@ const lint = job('lint', {
191
191
  });
192
192
 
193
193
  const test = job('test', {
194
- runsOn: 'ubuntu-latest',
194
+ runsOn: 'linux',
195
195
  needs: [lint],
196
196
  steps: [
197
197
  step('install', async ({ $ }) => {
@@ -264,9 +264,9 @@ Workflow: ci
264
264
  ✓ Matched trigger 1
265
265
  Jobs (2):
266
266
  lint
267
- runs-on: ubuntu-latest
267
+ runs-on: linux
268
268
  test
269
- runs-on: ubuntu-latest
269
+ runs-on: linux
270
270
 
271
271
  Decision Summary:
272
272
 
@@ -310,7 +310,7 @@ This updates `.kici/package.json` and generates (or updates) `package-lock.json`
310
310
 
311
311
  ### Dependency resolution contract
312
312
 
313
- Every `.kici/` dependency must be resolvable from the **single cloned repository**. When a job runs, the agent clones only this repository and installs `.kici/` dependencies with your repo's package manager (npm or pnpm yarn is not yet supported and is rejected with an actionable error). A dependency that points outside the cloned repo cannot be resolved.
313
+ Every `.kici/` dependency must be resolvable from the **single cloned repository**. When a job runs, the agent clones only this repository and installs `.kici/` dependencies with your repo's package manager (npm or pnpm; yarn is not supported the agent rejects it with an actionable error). A dependency that points outside the cloned repo cannot be resolved.
314
314
 
315
315
  In practice:
316
316
 
@@ -480,7 +480,7 @@ The lock file approach means the orchestrator stays git-agnostic -- it only need
480
480
 
481
481
  ## 5-minute quickstart
482
482
 
483
- Source: https://kici.dev/docs/user/quickstart/
483
+ Source: https://docs.kici.dev/user/quickstart/
484
484
 
485
485
  KiCI offers two equally-supported quickstart paths. Pick the one that fits your machine — both end with the same working pipeline (orchestrator + agent + your first workflow run visible in the dashboard).
486
486
 
@@ -517,7 +517,7 @@ Both quickstarts deploy a real orchestrator + agent. If you only want to write a
517
517
 
518
518
  ## Basic workflow patterns
519
519
 
520
- Source: https://kici.dev/docs/user/patterns/basic/
520
+ Source: https://docs.kici.dev/user/patterns/basic/
521
521
 
522
522
  A standard lint-then-test pipeline using job dependencies (`needs`):
523
523
 
@@ -525,7 +525,7 @@ A standard lint-then-test pipeline using job dependencies (`needs`):
525
525
  import { workflow, job, step, pr } from '@kici-dev/sdk';
526
526
 
527
527
  const lint = job('lint', {
528
- runsOn: 'ubuntu-latest',
528
+ runsOn: 'linux',
529
529
  steps: [
530
530
  step('install', async ({ $ }) => {
531
531
  await $`pnpm install --frozen-lockfile`;
@@ -538,7 +538,7 @@ const lint = job('lint', {
538
538
  });
539
539
 
540
540
  const test = job('test', {
541
- runsOn: 'ubuntu-latest',
541
+ runsOn: 'linux',
542
542
  needs: [lint],
543
543
  steps: [
544
544
  step('install', async ({ $ }) => {
@@ -551,7 +551,7 @@ const test = job('test', {
551
551
  });
552
552
 
553
553
  const typecheck = job('typecheck', {
554
- runsOn: 'ubuntu-latest',
554
+ runsOn: 'linux',
555
555
  needs: [lint],
556
556
  steps: [
557
557
  step('install', async ({ $ }) => {
@@ -608,7 +608,7 @@ const trigger = pr({
608
608
  });
609
609
 
610
610
  const build = job('build', {
611
- runsOn: 'ubuntu-latest',
611
+ runsOn: 'linux',
612
612
  steps: [
613
613
  step('build', async ({ $ }) => {
614
614
  await $`pnpm build`;
@@ -643,7 +643,7 @@ import { workflow, job, step, push } from '@kici-dev/sdk';
643
643
 
644
644
  // Deploy on pushes to main
645
645
  const deploy = job('deploy', {
646
- runsOn: 'ubuntu-latest',
646
+ runsOn: 'linux',
647
647
  steps: [
648
648
  step('deploy', async ({ $ }) => {
649
649
  await $`pnpm build`;
@@ -687,7 +687,7 @@ A workflow can respond to multiple trigger types:
687
687
  import { workflow, job, step, pr, push } from '@kici-dev/sdk';
688
688
 
689
689
  const test = job('test', {
690
- runsOn: 'ubuntu-latest',
690
+ runsOn: 'linux',
691
691
  steps: [
692
692
  step('test', async ({ $ }) => {
693
693
  await $`pnpm test`;
@@ -719,7 +719,7 @@ export default workflow('hello-world', {
719
719
  on: dispatch(),
720
720
  jobs: [
721
721
  job('greet', {
722
- runsOn: 'ubuntu-latest',
722
+ runsOn: 'linux',
723
723
  steps: [
724
724
  step('say-hello', async ({ $ }) => {
725
725
  await $`echo "Hello, World!"`;
@@ -767,7 +767,7 @@ Leave `dispatch()` unfiltered while you drive it from `kici run local`. The CLI
767
767
 
768
768
  ## Conditionals & matrix patterns
769
769
 
770
- Source: https://kici.dev/docs/user/patterns/conditionals-matrix/
770
+ Source: https://docs.kici.dev/user/patterns/conditionals-matrix/
771
771
 
772
772
  Rules control whether a workflow or job runs. Use `rule()` for conditions that must pass, and `skip()` for conditions that should skip execution.
773
773
 
@@ -777,7 +777,7 @@ Rules control whether a workflow or job runs. Use `rule()` for conditions that m
777
777
  import { workflow, job, step, pr, rule } from '@kici-dev/sdk';
778
778
 
779
779
  const test = job('test', {
780
- runsOn: 'ubuntu-latest',
780
+ runsOn: 'linux',
781
781
  steps: [
782
782
  step('test', async ({ $ }) => {
783
783
  await $`pnpm test`;
@@ -802,7 +802,7 @@ export default workflow('ci', {
802
802
  import { workflow, job, step, pr, rule, skip } from '@kici-dev/sdk';
803
803
 
804
804
  const unitTests = job('unit-tests', {
805
- runsOn: 'ubuntu-latest',
805
+ runsOn: 'linux',
806
806
  steps: [
807
807
  step('test', async ({ $ }) => {
808
808
  await $`pnpm test:unit`;
@@ -811,7 +811,7 @@ const unitTests = job('unit-tests', {
811
811
  });
812
812
 
813
813
  const e2eTests = job('e2e-tests', {
814
- runsOn: 'ubuntu-latest',
814
+ runsOn: 'linux',
815
815
  rules: [
816
816
  // Skip E2E when only docs change
817
817
  skip('docs only', async (ctx) => {
@@ -862,7 +862,7 @@ Run a job for each value in an array:
862
862
  import { workflow, job, step, push } from '@kici-dev/sdk';
863
863
 
864
864
  const test = job('test', {
865
- runsOn: 'ubuntu-latest',
865
+ runsOn: 'linux',
866
866
  matrix: ['18', '20', '22'],
867
867
  steps: [
868
868
  step('test', async ({ $, matrix }) => {
@@ -886,14 +886,14 @@ Use an object to define multiple dimensions. KiCI expands all combinations (capp
886
886
 
887
887
  ```typescript
888
888
  const test = job('test', {
889
- runsOn: 'ubuntu-latest',
889
+ runsOn: ['linux', 'kici:agent:container'],
890
890
  matrix: {
891
- os: ['ubuntu-latest', 'macos-latest'],
891
+ os: ['linux', 'arm64'],
892
892
  node: ['18', '20', '22'],
893
893
  },
894
894
  steps: [
895
895
  step('test', async ({ $, matrix }) => {
896
- // matrix.os = 'ubuntu-latest' | 'macos-latest'
896
+ // matrix.os = 'linux' | 'arm64'
897
897
  // matrix.node = '18' | '20' | '22'
898
898
  await $`echo "Testing on ${matrix!.os} with Node ${matrix!.node}"`;
899
899
  await $`pnpm test`;
@@ -904,21 +904,27 @@ const test = job('test', {
904
904
 
905
905
  This creates 6 job instances (2 OS x 3 Node versions).
906
906
 
907
+ > **Labels are customer-defined.** `runsOn` values such as `linux` or `arm64` are scaler
908
+ > labels **you** define in your orchestrator's `labelSets` — they are matched by subset
909
+ > semantics, not by a hosted-runner name. You can also target reserved auto-injected labels
910
+ > in the `kici:` namespace (e.g. `kici:agent:firecracker`, `kici:agent:container`) to pin a
911
+ > job to a specific backend type.
912
+
907
913
  ### Include and exclude
908
914
 
909
915
  Fine-tune matrix combinations:
910
916
 
911
917
  ```typescript
912
918
  const test = job('test', {
913
- runsOn: 'ubuntu-latest',
919
+ runsOn: 'linux',
914
920
  matrix: {
915
- os: ['ubuntu-latest', 'macos-latest', 'windows-latest'],
921
+ os: ['linux', 'arm64', 'windows'],
916
922
  node: ['18', '20', '22'],
917
923
  },
918
924
  // Remove specific combination
919
- exclude: [{ os: 'windows-latest', node: '18' }],
925
+ exclude: [{ os: 'windows', node: '18' }],
920
926
  // Add specific combination not in the matrix
921
- include: [{ os: 'ubuntu-latest', node: '23' }],
927
+ include: [{ os: 'linux', node: '23' }],
922
928
  steps: [
923
929
  step('test', async ({ $ }) => {
924
930
  await $`pnpm test`;
@@ -935,7 +941,7 @@ Compute matrix values at runtime using an async function:
935
941
 
936
942
  ```typescript
937
943
  const test = job('test', {
938
- runsOn: 'ubuntu-latest',
944
+ runsOn: 'linux',
939
945
  matrix: async ({ $ }) => {
940
946
  // Discover packages in a monorepo
941
947
  const result = await $`ls packages/`;
@@ -984,7 +990,7 @@ const discoverAndTest: DynamicJobFn = async ({ $ }) => {
984
990
 
985
991
  return packages.map((pkg) =>
986
992
  job(`test-${pkg}`, {
987
- runsOn: 'ubuntu-latest',
993
+ runsOn: 'linux',
988
994
  steps: [
989
995
  step('test', async ({ $ }) => {
990
996
  await $`cd packages/${pkg} && pnpm test`;
@@ -1006,7 +1012,7 @@ The `jobs` array accepts both static `Job` objects and `DynamicJobFn` functions:
1006
1012
 
1007
1013
  ```typescript
1008
1014
  const lint = job('lint', {
1009
- runsOn: 'ubuntu-latest',
1015
+ runsOn: 'linux',
1010
1016
  steps: [
1011
1017
  step('lint', async ({ $ }) => {
1012
1018
  await $`pnpm lint`;
@@ -1036,7 +1042,7 @@ const prTrigger = pr({ target: 'main', paths: ['src/**', 'packages/**', '!**/*.m
1036
1042
  const pushTrigger = push({ branches: 'main' });
1037
1043
 
1038
1044
  const lint = job('lint', {
1039
- runsOn: 'ubuntu-latest',
1045
+ runsOn: 'linux',
1040
1046
  steps: [
1041
1047
  step('install', async ({ $ }) => {
1042
1048
  await $`pnpm install --frozen-lockfile`;
@@ -1048,7 +1054,7 @@ const lint = job('lint', {
1048
1054
  });
1049
1055
 
1050
1056
  const test = job('test', {
1051
- runsOn: 'ubuntu-latest',
1057
+ runsOn: 'linux',
1052
1058
  needs: [lint],
1053
1059
  matrix: { node: ['18', '20', '22'] },
1054
1060
  steps: [
@@ -1059,7 +1065,7 @@ const test = job('test', {
1059
1065
  });
1060
1066
 
1061
1067
  const deploy = job('deploy', {
1062
- runsOn: 'ubuntu-latest',
1068
+ runsOn: 'linux',
1063
1069
  needs: [test],
1064
1070
  rules: [
1065
1071
  // Only deploy from push events (not PRs)
@@ -1098,7 +1104,7 @@ This workflow:
1098
1104
 
1099
1105
  ## Integration patterns
1100
1106
 
1101
- Source: https://kici.dev/docs/user/patterns/integrations/
1107
+ Source: https://docs.kici.dev/user/patterns/integrations/
1102
1108
 
1103
1109
  Use internal event triggers to chain workflows together. Workflow A completes, emits an event (or the system auto-emits a completion event), and Workflow B triggers in response.
1104
1110
 
@@ -1114,7 +1120,7 @@ export const deploy = workflow('deploy', {
1114
1120
  on: push({ branches: 'main' }),
1115
1121
  jobs: [
1116
1122
  job('deploy', {
1117
- runsOn: 'ubuntu-latest',
1123
+ runsOn: 'linux',
1118
1124
  steps: [
1119
1125
  step('deploy', async ({ $ }) => {
1120
1126
  await $`./scripts/deploy.sh`;
@@ -1129,7 +1135,7 @@ export const postDeploy = workflow('post-deploy', {
1129
1135
  on: workflowComplete({ name: 'deploy', status: ['success'] }),
1130
1136
  jobs: [
1131
1137
  job('notify', {
1132
- runsOn: 'ubuntu-latest',
1138
+ runsOn: 'linux',
1133
1139
  steps: [
1134
1140
  step('slack', async ({ $ }) => {
1135
1141
  await $`./scripts/notify-slack.sh "Deploy succeeded"`;
@@ -1152,7 +1158,7 @@ export const deploy = workflow('deploy', {
1152
1158
  on: push({ branches: 'main' }),
1153
1159
  jobs: [
1154
1160
  job('deploy', {
1155
- runsOn: 'ubuntu-latest',
1161
+ runsOn: 'linux',
1156
1162
  steps: [
1157
1163
  step('deploy', async ({ $ }) => {
1158
1164
  await $`./scripts/deploy.sh`;
@@ -1173,7 +1179,7 @@ export const postDeploy = workflow('post-deploy', {
1173
1179
  on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }),
1174
1180
  jobs: [
1175
1181
  job('smoke-test', {
1176
- runsOn: 'ubuntu-latest',
1182
+ runsOn: 'linux',
1177
1183
  steps: [
1178
1184
  step('test', async ({ $ }) => {
1179
1185
  await $`./scripts/smoke-test.sh`;
@@ -1198,7 +1204,7 @@ export default workflow('on-argocd-deploy', {
1198
1204
  on: genericWebhook({ source: 'argocd', events: ['deploy.success'] }),
1199
1205
  jobs: [
1200
1206
  job('post-deploy', {
1201
- runsOn: 'ubuntu-latest',
1207
+ runsOn: 'linux',
1202
1208
  steps: [
1203
1209
  step('verify', async ({ $, rawPayload }) => {
1204
1210
  // rawPayload contains the full webhook body from ArgoCD
@@ -1232,7 +1238,7 @@ export default workflow('stripe-invoice-handler', {
1232
1238
  }),
1233
1239
  jobs: [
1234
1240
  job('process-invoice', {
1235
- runsOn: 'ubuntu-latest',
1241
+ runsOn: 'linux',
1236
1242
  steps: [
1237
1243
  step('extract-customer', async ({ $, log }) => {
1238
1244
  log.info('Processing paid invoice from Stripe');
@@ -1289,7 +1295,7 @@ export default workflow('on-forgejo-push', {
1289
1295
  }),
1290
1296
  jobs: [
1291
1297
  job('react-to-push', {
1292
- runsOn: 'ubuntu-latest',
1298
+ runsOn: 'linux',
1293
1299
  steps: [
1294
1300
  step('log', async ({ rawPayload, log }) => {
1295
1301
  const ref = (rawPayload as { ref?: string }).ref;
@@ -1311,7 +1317,7 @@ Manual-clone example (pattern 2) using an SSH deploy key:
1311
1317
 
1312
1318
  ```typescript
1313
1319
  job('forgejo-ci', {
1314
- runsOn: 'ubuntu-latest',
1320
+ runsOn: 'linux',
1315
1321
  checkout: false, // skip framework clone
1316
1322
  steps: [
1317
1323
  step('clone', async ({ $, ctx, rawPayload }) => {
@@ -1356,7 +1362,7 @@ kici-admin source add generic \
1356
1362
  --rate-limit 120
1357
1363
 
1358
1364
  # Patch the verificationConfig to use GitHub's signature header
1359
- # (the CLI does not expose --signature-header yet; use the admin REST API):
1365
+ # (the CLI has no --signature-header flag; use the admin REST API):
1360
1366
  curl -X PATCH https://<orchestrator>/api/v1/admin/generic-sources/<sourceId> \
1361
1367
  -H "Authorization: Bearer <admin-token>" \
1362
1368
  -H "Content-Type: application/json" \
@@ -1383,7 +1389,7 @@ export default workflow('on-github-repo-push', {
1383
1389
  }),
1384
1390
  jobs: [
1385
1391
  job('notify', {
1386
- runsOn: 'ubuntu-latest',
1392
+ runsOn: 'linux',
1387
1393
  checkout: false, // no App token -> skip auto-clone
1388
1394
  steps: [
1389
1395
  step('log', async ({ rawPayload, log }) => {
@@ -1412,7 +1418,7 @@ export default workflow('on-github-repo-push', {
1412
1418
 
1413
1419
  ## Pattern reference
1414
1420
 
1415
- Source: https://kici.dev/docs/user/patterns/reference/
1421
+ Source: https://docs.kici.dev/user/patterns/reference/
1416
1422
 
1417
1423
  Every step receives a `StepContext` with these properties:
1418
1424
 
@@ -1490,7 +1496,7 @@ pnpm kici compile # Regenerates kici.lock.json with source locations
1490
1496
 
1491
1497
  ## Scheduling & event patterns
1492
1498
 
1493
- Source: https://kici.dev/docs/user/patterns/scheduling-and-events/
1499
+ Source: https://docs.kici.dev/user/patterns/scheduling-and-events/
1494
1500
 
1495
1501
  Run a full build and test suite on a schedule using `schedule()`. Schedule triggers are evaluated by the orchestrator's Raft leader in clustered deployments.
1496
1502
 
@@ -1502,7 +1508,7 @@ const install = step('install', async ({ $ }) => {
1502
1508
  });
1503
1509
 
1504
1510
  const fullTest = job('full-test', {
1505
- runsOn: 'ubuntu-latest',
1511
+ runsOn: 'linux',
1506
1512
  steps: [
1507
1513
  install,
1508
1514
  step('test', async ({ $ }) => {
@@ -1515,7 +1521,7 @@ const fullTest = job('full-test', {
1515
1521
  });
1516
1522
 
1517
1523
  const publish = job('publish-nightly', {
1518
- runsOn: 'ubuntu-latest',
1524
+ runsOn: 'linux',
1519
1525
  needs: [fullTest],
1520
1526
  steps: [
1521
1527
  install,
@@ -1559,7 +1565,7 @@ export const build = workflow('build', {
1559
1565
  on: push({ branches: 'main' }),
1560
1566
  jobs: [
1561
1567
  job('test', {
1562
- runsOn: 'ubuntu-latest',
1568
+ runsOn: 'linux',
1563
1569
  steps: [
1564
1570
  step('install', async ({ $ }) => {
1565
1571
  await $`pnpm install --frozen-lockfile`;
@@ -1580,7 +1586,7 @@ export const deploy = workflow('deploy-on-success', {
1580
1586
  on: workflowComplete({ name: 'build', status: ['success'] }),
1581
1587
  jobs: [
1582
1588
  job('deploy', {
1583
- runsOn: 'ubuntu-latest',
1589
+ runsOn: 'linux',
1584
1590
  steps: [
1585
1591
  step('deploy-staging', async ({ $ }) => {
1586
1592
  await $`./scripts/deploy.sh staging`;
@@ -1627,7 +1633,7 @@ export const testSuite = workflow('test-suite', {
1627
1633
  on: push({ branches: 'main' }),
1628
1634
  jobs: [
1629
1635
  job('test', {
1630
- runsOn: 'ubuntu-latest',
1636
+ runsOn: 'linux',
1631
1637
  steps: [
1632
1638
  step('install', async ({ $ }) => {
1633
1639
  await $`pnpm install --frozen-lockfile`;
@@ -1653,7 +1659,7 @@ export const autoDeploy = workflow('auto-deploy', {
1653
1659
  on: kiciEvent({ name: 'tests-passed' }),
1654
1660
  jobs: [
1655
1661
  job('deploy', {
1656
- runsOn: 'ubuntu-latest',
1662
+ runsOn: 'linux',
1657
1663
  steps: [
1658
1664
  step('deploy', async ({ $ }) => {
1659
1665
  await $`./scripts/deploy.sh`;
@@ -1682,9 +1688,152 @@ export const autoDeploy = workflow('auto-deploy', {
1682
1688
 
1683
1689
  # SDK reference
1684
1690
 
1691
+ ## Caching
1692
+
1693
+ Source: https://docs.kici.dev/user/sdk/caching/
1694
+
1695
+ KiCI ships a general-purpose cache for any files or directories your workflow produces — compiled artifacts, downloaded toolchains, package manager stores, build outputs. A cache entry is keyed, immutable once written, and shared across runs of the same repository so a later run can restore what an earlier run produced instead of recomputing it.
1696
+
1697
+ Two surfaces drive the same cache:
1698
+
1699
+ - **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.
1700
+ - **Imperative** — `ctx.cache.restore(spec)` / `ctx.cache.save(spec)` inside a step body, for fine-grained control over when restore and save happen.
1701
+
1702
+ 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.
1703
+
1704
+ ## CacheSpec
1705
+
1706
+ Both surfaces take the same shape:
1707
+
1708
+ ```typescript
1709
+ interface CacheSpec {
1710
+ /** Exact cache key. First save wins; re-saving an existing key is a no-op. */
1711
+ key: string;
1712
+ /** Files/directories to cache. Repo-root-relative or `~`-prefixed. */
1713
+ paths: string[];
1714
+ /** Ordered prefix fallbacks for partial restore; newest matching entry wins. */
1715
+ restoreKeys?: string[];
1716
+ }
1717
+ ```
1718
+
1719
+ - **`key`** is the exact cache key. It is **immutable** — the first save under a given key wins, and any later save under the same exact key is a no-op (the existing entry is never overwritten). Build keys from inputs that change when the cached content should change, e.g. a hash of your lockfile: `` key: `deps-${await ctx.$`sha256sum pnpm-lock.yaml`}` ``.
1720
+ - **`paths`** are the files and directories to archive, repo-root-relative or `~`-prefixed (the agent expands `~` to the workspace home). At least one path is required.
1721
+ - **`restoreKeys`** are ordered **prefix** fallbacks tried only when the exact `key` misses on restore. Each prefix is matched against existing entries; the **newest** matching entry wins. This lets a run that changed its lockfile still restore the closest previous cache and rebuild incrementally.
1722
+
1723
+ ## Declarative cache
1724
+
1725
+ Add a `cache` field to a job or a step. It accepts one `CacheSpec` or an array of them. The runtime restores every spec before the job/step runs (surfaced as a `cache:restore` pseudo-step) and saves every spec after it completes successfully (surfaced as a `cache:save` pseudo-step):
1726
+
1727
+ ```typescript
1728
+ import { job } from '@kici-dev/sdk';
1729
+
1730
+ job('build', {
1731
+ runsOn: 'linux-x64',
1732
+ cache: {
1733
+ key: 'mise-tools-v1',
1734
+ paths: ['~/.local/share/mise'],
1735
+ },
1736
+ steps: [
1737
+ step('install-tools', async (ctx) => {
1738
+ await ctx.$`mise install`;
1739
+ }),
1740
+ step('build', async (ctx) => {
1741
+ await ctx.$`mise exec -- pnpm build`;
1742
+ }),
1743
+ ],
1744
+ });
1745
+ ```
1746
+
1747
+ Step-level cache scopes the restore/save to a single step:
1748
+
1749
+ ```typescript
1750
+ step('deps', {
1751
+ cache: { key: `npm-${lockfileHash}`, paths: ['node_modules'], restoreKeys: ['npm-'] },
1752
+ run: async (ctx) => {
1753
+ await ctx.$`pnpm install --frozen-lockfile`;
1754
+ },
1755
+ });
1756
+ ```
1757
+
1758
+ On a cache **hit**, the archived paths are restored before the step body runs, so `pnpm install` sees a warm `node_modules`. On a **miss**, the step runs cold and the resulting paths are saved under the exact key for the next run.
1759
+
1760
+ ## Imperative cache (`ctx.cache`)
1761
+
1762
+ When you need to decide at runtime whether to restore or save — for example, save only when a build actually changed something — use the imperative API on the step context:
1763
+
1764
+ ```typescript
1765
+ step('build', async (ctx) => {
1766
+ const result = await ctx.cache.restore({
1767
+ key: `build-${sourceHash}`,
1768
+ paths: ['dist'],
1769
+ restoreKeys: ['build-'],
1770
+ });
1771
+
1772
+ if (result.hit) {
1773
+ ctx.log.info(`restored cache (matched ${result.matchedKey})`);
1774
+ }
1775
+
1776
+ await ctx.$`pnpm build`;
1777
+
1778
+ await ctx.cache.save({ key: `build-${sourceHash}`, paths: ['dist'] });
1779
+ });
1780
+ ```
1781
+
1782
+ `restore(spec)` returns `{ hit, matchedKey? }`:
1783
+
1784
+ - `hit` is `true` when the exact `key` matched **or** a `restoreKeys` prefix matched.
1785
+ - `matchedKey` is the full key that actually matched — the exact key on a direct hit, or the full key of the matched prefix entry on a fallback hit.
1786
+
1787
+ `save(spec)` archives `spec.paths` under `spec.key`. Like the declarative surface, it is immutable: the first save under an exact key wins, and re-saving the same key is a no-op.
1788
+
1789
+ ## Restore semantics
1790
+
1791
+ A restore resolves in this order:
1792
+
1793
+ 1. **Exact key.** If an entry exists under the exact `key`, it is restored and `matchedKey === key`.
1794
+ 2. **restoreKeys prefix fallback.** Each `restoreKeys` prefix is tried in order. Within a prefix, the **newest** matching entry wins; `matchedKey` is that entry's full key.
1795
+ 3. **Miss.** If nothing matches, `hit` is `false` and no paths are restored.
1796
+
1797
+ This mirrors the familiar lockfile-hash pattern: key the entry on the exact lockfile hash, and add a `restoreKeys` prefix so a changed lockfile still restores the most recent prior cache to rebuild from.
1798
+
1799
+ ## Immutability
1800
+
1801
+ Cache keys are write-once. The **first** save under an exact key wins; every subsequent save under that same exact key is a no-op and the original bytes are preserved. To publish new content, use a new key (typically by including a content hash in the key). Immutability is what makes a cache hit safe to trust — the bytes behind a given key never change after they are first written.
1802
+
1803
+ ## Isolation
1804
+
1805
+ Each cache entry is scoped to your organization and to the ref's trust level:
1806
+
1807
+ - **Trusted refs** (your repository's own branches, default branch) read and write a **shared** scope visible to the whole org for that repository.
1808
+ - **Untrusted / fork refs** read the shared scope as a fallback but write to an **isolated** per-run scope. A fork build can therefore benefit from a warm cache the trusted branch produced, but can never write into the shared scope — so a malicious fork cannot poison the cache a trusted branch later restores.
1809
+
1810
+ No tenant can read another tenant's cache; the org boundary is enforced in the cache key namespace.
1811
+
1812
+ ## Eviction
1813
+
1814
+ Cache storage is bounded per organization. Two mechanisms keep it bounded:
1815
+
1816
+ - **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.
1817
+ - **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.
1818
+
1819
+ Both knobs are operator-configured on the orchestrator — see [orchestrator storage layout](../../operator/orchestrator/storage-layout.md).
1820
+
1821
+ ## Observability
1822
+
1823
+ 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.
1824
+
1825
+ ## See also
1826
+
1827
+ - [Core](./core.md) -- `job()` / `step()` factories the `cache` field attaches to
1828
+ - [Runtime](./runtime.md) -- `StepContext`, where `ctx.cache` lives
1829
+ - [Orchestrator storage layout](../../operator/orchestrator/storage-layout.md) -- cache prefix, quota, TTL, and eviction
1830
+ - [Data flows](../../architecture/data-flows.md#user-facing-cache-flow) -- restore/save protocol and trust→scope mapping
1831
+
1832
+ ---
1833
+
1685
1834
  ## SDK reference: core
1686
1835
 
1687
- Source: https://kici.dev/docs/user/sdk/core/
1836
+ Source: https://docs.kici.dev/user/sdk/core/
1688
1837
 
1689
1838
  ## Factory functions
1690
1839
 
@@ -1698,21 +1847,22 @@ function workflow(name: string, options: WorkflowOptions): Workflow;
1698
1847
 
1699
1848
  **Parameters:**
1700
1849
 
1701
- | Parameter | Type | Required | Description |
1702
- | --------------------- | ---------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
1703
- | `name` | `string` | yes | Unique workflow name |
1704
- | `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators |
1705
- | `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger |
1706
- | `options.rules` | `Rule[]` | no | Conditions that must pass for execution |
1707
- | `options.description` | `string` | no | Human-readable description |
1708
- | `options.hashFiles` | `string[]` | no | Extra repo-relative paths or globs mixed into the workflow content hash. Changes invalidate the source cache. |
1709
- | `options.registries` | `Registry[]` | no | Private npm registries the agent authenticates against before `npm install`. Each `tokenSecret` uses qualified `<environment>:<secret>` syntax. |
1710
- | `options.installEnv` | `string[]` | no | Qualified `<environment>:<secret>` refs projected as env vars onto the install subprocess (used with a customer-committed `.kici/.npmrc`). |
1711
- | `options.onCancel` | `HookInput` | no | Runs when the workflow is cancelled |
1712
- | `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel) |
1713
- | `options.onSuccess` | `HookInput` | no | Runs on workflow success |
1714
- | `options.onFailure` | `HookInput` | no | Runs on workflow failure |
1715
- | `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](../concurrency.md). |
1850
+ | Parameter | Type | Required | Description |
1851
+ | --------------------- | ---------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
1852
+ | `name` | `string` | yes | Unique workflow name |
1853
+ | `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators |
1854
+ | `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger |
1855
+ | `options.rules` | `Rule[]` | no | Conditions that must pass for execution |
1856
+ | `options.description` | `string` | no | Human-readable description |
1857
+ | `options.hashFiles` | `string[]` | no | Extra repo-relative paths or globs mixed into the workflow content hash. Changes invalidate the source cache. |
1858
+ | `options.registries` | `Registry[]` | no | Private npm registries the agent authenticates against before `npm install`. Each `tokenSecret` uses qualified `<environment>:<secret>` syntax. |
1859
+ | `options.installEnv` | `string[]` | no | Qualified `<environment>:<secret>` refs projected as env vars onto the install subprocess (used with a customer-committed `.kici/.npmrc`). |
1860
+ | `options.onCancel` | `HookInput` | no | Runs when the workflow is cancelled |
1861
+ | `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel) |
1862
+ | `options.onSuccess` | `HookInput` | no | Runs on workflow success |
1863
+ | `options.onFailure` | `HookInput` | no | Runs on workflow failure |
1864
+ | `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](../concurrency.md). |
1865
+ | `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). |
1716
1866
 
1717
1867
  **Returns:** `Workflow` -- an immutable workflow definition.
1718
1868
 
@@ -1738,45 +1888,47 @@ function job(options: JobOptions): Job;
1738
1888
 
1739
1889
  **Parameters:**
1740
1890
 
1741
- | Parameter | Type | Required | Description |
1742
- | -------------------------- | --------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
1743
- | `name` | `string` | no | Job name (auto-generated UUID if omitted) |
1744
- | `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
1745
- | `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
1746
- | `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`. |
1747
- | `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](#job-dependencies-needs) |
1748
- | `options.rules` | `Rule[]` | no | Conditions for conditional execution |
1749
- | `options.description` | `string` | no | Human-readable description |
1750
- | `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
1751
- | `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
1752
- | `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
1753
- | `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
1754
- | `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. |
1755
- | `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). |
1756
- | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
1757
- | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](../concurrency.md). |
1758
- | `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
1759
- | `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
1760
- | `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
1761
- | `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
1762
- | `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
1763
- | `options.afterStep` | `HookInput` | no | Hook that runs after each step |
1764
- | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](../hooks.md#hook-timeout). |
1765
- | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](#per-job-resources) below. |
1891
+ | Parameter | Type | Required | Description |
1892
+ | -------------------------- | --------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
1893
+ | `name` | `string` | no | Job name (auto-generated UUID if omitted) |
1894
+ | `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
1895
+ | `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
1896
+ | `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](#single-step-job-shorthand). Mutually exclusive with `steps`. |
1897
+ | `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](#job-dependencies-needs) |
1898
+ | `options.rules` | `Rule[]` | no | Conditions for conditional execution |
1899
+ | `options.description` | `string` | no | Human-readable description |
1900
+ | `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
1901
+ | `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
1902
+ | `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
1903
+ | `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
1904
+ | `options.container` | `string \| ContainerConfig` | no | Docker image for job execution. String form is the image name; object form adds `env`. All steps run inside the container. |
1905
+ | `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
1906
+ | `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
1907
+ | `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](../concurrency.md). |
1908
+ | `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
1909
+ | `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
1910
+ | `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
1911
+ | `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
1912
+ | `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
1913
+ | `options.afterStep` | `HookInput` | no | Hook that runs after each step |
1914
+ | `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](../hooks.md#hook-timeout). |
1915
+ | `options.timeout` | `number` | no | Total job wall-clock timeout in milliseconds (init + all steps + hooks). On breach the job is aborted and reported timed out. See [Timeouts](#timeouts). |
1916
+ | `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](#per-job-resources) below. |
1917
+ | `options.init` | `GenericInitConfig \| GenericInitConfig[] \| false` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. See [Per-job init](#per-job-init) below. |
1766
1918
 
1767
1919
  **Returns:** `Job` -- an immutable job definition.
1768
1920
 
1769
1921
  ```typescript
1770
1922
  // Named job
1771
1923
  const build = job('build', {
1772
- runsOn: 'ubuntu-latest',
1924
+ runsOn: 'linux',
1773
1925
  steps: [checkout, install, compile],
1774
1926
  needs: [lint],
1775
1927
  });
1776
1928
 
1777
1929
  // Anonymous job (auto-generated UUID name)
1778
1930
  const build = job({
1779
- runsOn: 'ubuntu-latest',
1931
+ runsOn: 'linux',
1780
1932
  steps: [checkout, install],
1781
1933
  });
1782
1934
  ```
@@ -1802,7 +1954,7 @@ runsOn: { labels: ['linux', 'docker'], exclude: ['gpu'] }
1802
1954
  - **Required labels:** The agent must have every label in the `labels` array (or the string/array form).
1803
1955
  - **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.
1804
1956
  - **Compile-time validation:** The compiler will error if any label appears in both `labels` and `exclude` (overlap detection).
1805
- - **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 operator guide](/operator/orchestrator/auto-scaler/overview-and-config/#mandatory-labels-k8s-style-opt-in-gate) for details.
1957
+ - **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.
1806
1958
 
1807
1959
  ```typescript
1808
1960
  // Route to any Linux agent that does NOT have the 'gpu' label
@@ -1899,6 +2051,65 @@ Per-backend kernel enforcement of `limits`:
1899
2051
  - **Firecracker backend:** always enforced. Fractional CPU rounds up to the nearest integer vCPU.
1900
2052
  - **Bare-metal backend:** advisory by default — the scaler caps still apply, but no cgroup is created. Operators can opt in to kernel enforcement via `enforceCgroups: true` on the scaler entry.
1901
2053
 
2054
+ ### Per-job init
2055
+
2056
+ `options.init` declares a hand-written command that runs **after the repo is cloned and before the job's steps execute**. Its purpose is to provision a repo-declared toolchain (a `mise` toolchain, a custom setup script, a language runtime) and put it on the environment every subsequent step sees.
2057
+
2058
+ ```typescript
2059
+ import { workflow, job, step, push } from '@kici-dev/sdk';
2060
+
2061
+ export const build = workflow('build', {
2062
+ on: [push()],
2063
+ jobs: [
2064
+ job('build', {
2065
+ runsOn: 'linux',
2066
+ init: {
2067
+ run: `
2068
+ set -euo pipefail
2069
+ command -v mise >/dev/null || curl -fsSL https://mise.run | sh
2070
+ export PATH="$HOME/.local/bin:$PATH"
2071
+ mise install
2072
+ mise env -s bash | sed -n 's/^export //p' >> "$KICI_ENV"
2073
+ echo "$HOME/.local/share/mise/shims" >> "$KICI_PATH"
2074
+ `,
2075
+ cache: { key: 'mise-jq-1.7.1', paths: ['~/.local/share/mise'] },
2076
+ timeout: 600_000,
2077
+ },
2078
+ steps: [
2079
+ step('show-jq-version', async (ctx) => {
2080
+ // jq is on PATH because the init phase appended the mise shims dir to $KICI_PATH.
2081
+ const { stdout } = await ctx.$`jq --version`;
2082
+ ctx.log.info(`jq version: ${stdout.trim()}`);
2083
+ }),
2084
+ ],
2085
+ }),
2086
+ ],
2087
+ });
2088
+ ```
2089
+
2090
+ **`GenericInitConfig` shape:**
2091
+
2092
+ | Field | Type | Required | Description |
2093
+ | --------- | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
2094
+ | `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. |
2095
+ | `shell` | `string` | no | Shell used to run `run`. Defaults to `bash`. |
2096
+ | `cache` | `CacheSpec` | no | Cache spec for binaries the command installs -- restored before the command, saved after on a key miss. See [Caching](./caching.md). |
2097
+ | `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. |
2098
+ | `env` | `Record<string,string>` | no | Static environment variables available to the command. |
2099
+
2100
+ **The `$KICI_ENV` / `$KICI_PATH` handoff.** The init command does not mutate the step environment directly. Instead it writes what it wants visible to later steps to two files the agent allocates and exposes as environment variables:
2101
+
2102
+ - **`$KICI_ENV`** -- append one `KEY=value` line per environment variable. The agent reads the file after the command and makes each variable available to every subsequent step.
2103
+ - **`$KICI_PATH`** -- append one directory per line. The agent prepends each directory to `PATH` for every subsequent step.
2104
+
2105
+ The agent reads both files after the command succeeds, applies the delta, and the resulting environment is visible to all steps that follow (and to any later init command).
2106
+
2107
+ **Failure before steps.** If the init command exits non-zero or exceeds its `timeout`, the job **fails before any step runs** -- the init surfaces as a failed `init:<n>` pseudo-step in the run timeline (alongside the step list), its logs are attached, and the step loop never executes. This makes a broken toolchain a clear, early failure rather than a confusing mid-run error.
2108
+
2109
+ **Arrays run in order.** Passing `GenericInitConfig[]` runs the inits sequentially; each one's `$KICI_ENV` / `$KICI_PATH` delta is applied before the next runs, so a later init sees an earlier init's tools on `PATH`. The first init to fail stops the sequence and fails the job.
2110
+
2111
+ **`init: false`** is an explicit opt-out (reserved for a future auto-detect layer); it behaves the same as omitting `init`.
2112
+
1902
2113
  ## Step & job authoring patterns
1903
2114
 
1904
2115
  KiCI supports several authoring patterns for steps and jobs to reduce boilerplate and improve developer experience.
@@ -1997,6 +2208,44 @@ const deploy = job('deploy', {
1997
2208
 
1998
2209
  The `run` function is stored as the job's only step with an auto-generated name (`step-1`). `run` and `steps` are mutually exclusive -- providing both throws an error.
1999
2210
 
2211
+ ### Timeouts
2212
+
2213
+ `timeout` (milliseconds) can be set at three levels. Each level caps **its own scope** independently — a workflow or job timeout is a separate wall-clock cap, **not** a default that flows down to steps.
2214
+
2215
+ | Level | Field | Caps | Enforced by | On breach |
2216
+ | ------------ | ---------------------------- | ------------------------------------------------------ | ---------------- | ----------------------------------------------------------------- |
2217
+ | **step** | `step(..., { timeout })` | A single step's wall-clock. | the agent | The step fails; falls back to the 30-minute default when unset. |
2218
+ | **job** | `job(..., { timeout })` | The job's total wall-clock (init + all steps + hooks). | the agent | The job is aborted and reported failed with a "timed out" reason. |
2219
+ | **workflow** | `workflow(..., { timeout })` | The whole run's wall-clock across all jobs. | the orchestrator | The run is cancelled with a "timed out" reason. |
2220
+
2221
+ ```typescript
2222
+ export default workflow('ci', {
2223
+ timeout: 1_800_000, // whole run must finish within 30 minutes
2224
+ jobs: [
2225
+ job('build', {
2226
+ runsOn: 'linux',
2227
+ timeout: 600_000, // this job (init + steps + hooks) within 10 minutes
2228
+ steps: [
2229
+ step('compile', {
2230
+ timeout: 120_000, // this single step within 2 minutes
2231
+ run: async (ctx) => {
2232
+ await ctx.$`make build`;
2233
+ },
2234
+ }),
2235
+ ],
2236
+ }),
2237
+ ],
2238
+ });
2239
+ ```
2240
+
2241
+ **Precedence — each scope caps its own scope.** The three timeouts are independent caps, not a fallback chain:
2242
+
2243
+ - A **step** with no `timeout` falls back to the 30-minute agent default, regardless of the job or workflow timeout. A job timeout never becomes a step's default.
2244
+ - A **job** `timeout` bounds the job's total wall-clock (its init, every step including their own per-step timeouts, and its hooks). It does not change any step's individual cap.
2245
+ - A **workflow** `timeout` is a run-level deadline. The orchestrator records it when the run starts and cancels the run if its wall-clock exceeds the timeout, even when individual jobs and steps are still within their own caps.
2246
+
2247
+ 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.
2248
+
2000
2249
  ### Output chaining
2001
2250
 
2002
2251
  Steps and jobs can access outputs from preceding steps/jobs using two patterns.
@@ -2169,9 +2418,357 @@ These IDs are stable as long as the order of unnamed entries does not change. Ad
2169
2418
 
2170
2419
  ---
2171
2420
 
2421
+ ## Event payload reference
2422
+
2423
+ Source: https://docs.kici.dev/user/sdk/event-payloads/
2424
+
2425
+ <!-- Generated by scripts/docs-gen-event-payloads.ts — do not edit by hand. Regenerate: pnpm docs:gen:events -->
2426
+
2427
+ ## The envelope
2428
+
2429
+ The normalized event envelope is the single event contract in KiCI. Rules receive it as `ctx.event`, and every dynamic function — `environment:`, `env:`, and `concurrencyGroup:` resolvers, generated jobs, and a workflow's `concurrency.group` — receives the same envelope as its argument.
2430
+
2431
+ Narrow on the `type` field to branch per trigger kind (`if (event.type === 'push')`). The raw provider webhook body is nested at `payload`; the typed variants below describe its shape per event type.
2432
+
2433
+ These fields are present on every envelope (the `EventBase` shape):
2434
+
2435
+ | Field | Type | Description |
2436
+ | ----------------- | ------------------------- | ------------------------------------------------------------------------------- |
2437
+ | `type` | `string` | Normalized event type discriminant. |
2438
+ | `action?` | `string` | Sub-action (e.g. 'opened', 'created', 'submitted'). |
2439
+ | `targetBranch?` | `string` | Target branch (push target, PR base, or default branch). |
2440
+ | `sourceBranch?` | `string` | Source branch (PR head branch). Only set for PR-like events. |
2441
+ | `provider?` | `string` | Provider that originated this event. |
2442
+ | `isForkPR?` | `boolean` | Whether this PR comes from a fork. Only set for PR-like events. |
2443
+ | `baseBranch?` | `string` | Base branch ref for PR events. |
2444
+ | `senderUsername?` | `string` | Sender username from the webhook payload. |
2445
+ | `sourceRepo?` | `string` | Repository identifier (e.g. "owner/repo"). |
2446
+ | `changedFiles?` | `string[]` | Files changed in this event (for path filtering). |
2447
+ | `payload?` | `Record<string, unknown>` | Raw webhook payload from the provider. May be absent in flattened event forms. |
2448
+ | `[key: string]` | `unknown` | Index signature for backward compatibility — untyped fields resolve to unknown. |
2449
+
2450
+ ## Event types
2451
+
2452
+ One section per member of the `EventPayload` union. The heading is the `type` literal; the table lists the fields of that event's `payload` property when it declares a typed shape.
2453
+
2454
+ ### `pull_request`
2455
+
2456
+ Carried by `PullRequestEventPayload`. The `payload` property has the following shape:
2457
+
2458
+ | Field | Type | Description |
2459
+ | --------------- | ------------------- | ----------- |
2460
+ | `action` | `string` | |
2461
+ | `number` | `number` | |
2462
+ | `pull_request` | `GitHubPullRequest` | |
2463
+ | `repository` | `GitHubRepository` | |
2464
+ | `sender` | `GitHubUser` | |
2465
+ | `[key: string]` | `unknown` | |
2466
+
2467
+ ### `push`
2468
+
2469
+ Carried by `PushEventPayload`. The `payload` property has the following shape:
2470
+
2471
+ | Field | Type | Description |
2472
+ | --------------- | ------------------ | ----------- |
2473
+ | `ref` | `string` | |
2474
+ | `after` | `string` | |
2475
+ | `before` | `string` | |
2476
+ | `head_commit?` | `GitHubCommit` | |
2477
+ | `commits?` | `GitHubCommit[]` | |
2478
+ | `repository` | `GitHubRepository` | |
2479
+ | `sender?` | `GitHubUser` | |
2480
+ | `forced?` | `boolean` | |
2481
+ | `[key: string]` | `unknown` | |
2482
+
2483
+ ### `tag`
2484
+
2485
+ Carried by `TagEventPayload`. The `payload` property has the following shape:
2486
+
2487
+ | Field | Type | Description |
2488
+ | --------------- | ------------------ | ----------- |
2489
+ | `ref` | `string` | |
2490
+ | `after` | `string` | |
2491
+ | `repository` | `GitHubRepository` | |
2492
+ | `sender?` | `GitHubUser` | |
2493
+ | `[key: string]` | `unknown` | |
2494
+
2495
+ ### `comment`
2496
+
2497
+ Carried by `CommentEventPayload`. The `payload` property has the following shape:
2498
+
2499
+ | Field | Type | Description |
2500
+ | --------------- | ------------------------------------------------------------------------------------ | ----------- |
2501
+ | `action` | `string` | |
2502
+ | `comment` | `GitHubComment` | |
2503
+ | `issue?` | `{ number: number; title?: string; pull_request?: unknown; [key: string]: unknown }` | |
2504
+ | `repository` | `GitHubRepository` | |
2505
+ | `sender` | `GitHubUser` | |
2506
+ | `[key: string]` | `unknown` | |
2507
+
2508
+ ### `review`
2509
+
2510
+ Carried by `ReviewEventPayload`. The `payload` property has the following shape:
2511
+
2512
+ | Field | Type | Description |
2513
+ | --------------- | ------------------- | ----------- |
2514
+ | `action` | `string` | |
2515
+ | `review` | `GitHubReview` | |
2516
+ | `pull_request` | `GitHubPullRequest` | |
2517
+ | `repository` | `GitHubRepository` | |
2518
+ | `sender` | `GitHubUser` | |
2519
+ | `[key: string]` | `unknown` | |
2520
+
2521
+ ### `review_comment`
2522
+
2523
+ Carried by `ReviewCommentEventPayload`. The `payload` property has the following shape:
2524
+
2525
+ | Field | Type | Description |
2526
+ | --------------- | ------------------- | ----------- |
2527
+ | `action` | `string` | |
2528
+ | `comment` | `GitHubComment` | |
2529
+ | `pull_request` | `GitHubPullRequest` | |
2530
+ | `repository` | `GitHubRepository` | |
2531
+ | `sender` | `GitHubUser` | |
2532
+ | `[key: string]` | `unknown` | |
2533
+
2534
+ ### `release`
2535
+
2536
+ Carried by `ReleaseEventPayload`. The `payload` property has the following shape:
2537
+
2538
+ | Field | Type | Description |
2539
+ | --------------- | ------------------ | ----------- |
2540
+ | `action` | `string` | |
2541
+ | `release` | `GitHubRelease` | |
2542
+ | `repository` | `GitHubRepository` | |
2543
+ | `sender` | `GitHubUser` | |
2544
+ | `[key: string]` | `unknown` | |
2545
+
2546
+ ### `dispatch`
2547
+
2548
+ Carried by `DispatchEventPayload`. The `payload` property has the following shape:
2549
+
2550
+ | Field | Type | Description |
2551
+ | ----------------- | ------------------------- | ----------- |
2552
+ | `action` | `string` | |
2553
+ | `client_payload?` | `Record<string, unknown>` | |
2554
+ | `repository` | `GitHubRepository` | |
2555
+ | `sender?` | `GitHubUser` | |
2556
+ | `[key: string]` | `unknown` | |
2557
+
2558
+ ### `create`
2559
+
2560
+ Carried by `CreateEventPayload`. The `payload` property has the following shape:
2561
+
2562
+ | Field | Type | Description |
2563
+ | --------------- | ------------------ | ----------- |
2564
+ | `ref` | `string` | |
2565
+ | `ref_type` | `string` | |
2566
+ | `repository` | `GitHubRepository` | |
2567
+ | `sender` | `GitHubUser` | |
2568
+ | `[key: string]` | `unknown` | |
2569
+
2570
+ ### `delete`
2571
+
2572
+ Carried by `DeleteEventPayload`. The `payload` property has the following shape:
2573
+
2574
+ | Field | Type | Description |
2575
+ | --------------- | ------------------ | ----------- |
2576
+ | `ref` | `string` | |
2577
+ | `ref_type` | `string` | |
2578
+ | `repository` | `GitHubRepository` | |
2579
+ | `sender` | `GitHubUser` | |
2580
+ | `[key: string]` | `unknown` | |
2581
+
2582
+ ### `status`
2583
+
2584
+ Carried by `StatusEventPayload`. The `payload` property has the following shape:
2585
+
2586
+ | Field | Type | Description |
2587
+ | --------------- | ------------------------------------------------- | ----------- |
2588
+ | `state` | `string` | |
2589
+ | `sha` | `string` | |
2590
+ | `context` | `string` | |
2591
+ | `description?` | `string` | |
2592
+ | `target_url?` | `string` | |
2593
+ | `branches?` | `Array<{ name: string; [key: string]: unknown }>` | |
2594
+ | `repository` | `GitHubRepository` | |
2595
+ | `sender` | `GitHubUser` | |
2596
+ | `[key: string]` | `unknown` | |
2597
+
2598
+ ### `workflow_run`
2599
+
2600
+ Carried by `WorkflowRunEventPayload`. The `payload` property has the following shape:
2601
+
2602
+ | Field | Type | Description |
2603
+ | --------------- | ------------------------------------------------------------------------------------------------------ | ----------- |
2604
+ | `action` | `string` | |
2605
+ | `workflow_run` | `{ head_branch: string; name: string; conclusion?: string; status?: string; [key: string]: unknown; }` | |
2606
+ | `repository` | `GitHubRepository` | |
2607
+ | `sender` | `GitHubUser` | |
2608
+ | `[key: string]` | `unknown` | |
2609
+
2610
+ ### `fork`
2611
+
2612
+ Carried by `ForkEventPayload`. The `payload` property has the following shape:
2613
+
2614
+ | Field | Type | Description |
2615
+ | --------------- | ----------------------------------------------- | ----------- |
2616
+ | `forkee` | `{ full_name: string; [key: string]: unknown }` | |
2617
+ | `repository` | `GitHubRepository` | |
2618
+ | `sender` | `GitHubUser` | |
2619
+ | `[key: string]` | `unknown` | |
2620
+
2621
+ ### `star`
2622
+
2623
+ Carried by `StarEventPayload`. The `payload` property has the following shape:
2624
+
2625
+ | Field | Type | Description |
2626
+ | --------------- | ------------------ | ----------- |
2627
+ | `action` | `string` | |
2628
+ | `repository` | `GitHubRepository` | |
2629
+ | `sender` | `GitHubUser` | |
2630
+ | `[key: string]` | `unknown` | |
2631
+
2632
+ ### `watch`
2633
+
2634
+ Carried by `WatchEventPayload`. The `payload` property has the following shape:
2635
+
2636
+ | Field | Type | Description |
2637
+ | --------------- | ------------------ | ----------- |
2638
+ | `action` | `string` | |
2639
+ | `repository` | `GitHubRepository` | |
2640
+ | `sender` | `GitHubUser` | |
2641
+ | `[key: string]` | `unknown` | |
2642
+
2643
+ ### `webhook`
2644
+
2645
+ Carried by `WebhookEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2646
+
2647
+ ### `kici_event`
2648
+
2649
+ Carried by `KiciEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2650
+
2651
+ ### `workflow_complete`
2652
+
2653
+ Carried by `WorkflowCompleteEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2654
+
2655
+ ### `job_complete`
2656
+
2657
+ Carried by `JobCompleteEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2658
+
2659
+ ### `generic_webhook`
2660
+
2661
+ Carried by `GenericWebhookEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2662
+
2663
+ ### `schedule`
2664
+
2665
+ Carried by `ScheduleEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2666
+
2667
+ ### `lifecycle`
2668
+
2669
+ Carried by `LifecycleEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2670
+
2671
+ ### `rerun`
2672
+
2673
+ Carried by `RerunEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2674
+
2675
+ ### `manual_schedule`
2676
+
2677
+ Carried by `ManualScheduleEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2678
+
2679
+ ### `unknown`
2680
+
2681
+ Carried by `UnknownEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
2682
+
2683
+ ## Shared GitHub object shapes
2684
+
2685
+ The typed `payload` shapes above reference these partial GitHub object types. Each lists only the commonly accessed fields; the index signature on every shape resolves any other field to `unknown`.
2686
+
2687
+ ### `GitHubRepository`
2688
+
2689
+ | Field | Type | Description |
2690
+ | ---------------- | ------------------------------------------- | ----------- |
2691
+ | `full_name` | `string` | |
2692
+ | `default_branch` | `string` | |
2693
+ | `name?` | `string` | |
2694
+ | `owner?` | `{ login: string; [key: string]: unknown }` | |
2695
+ | `private?` | `boolean` | |
2696
+ | `[key: string]` | `unknown` | |
2697
+
2698
+ ### `GitHubUser`
2699
+
2700
+ | Field | Type | Description |
2701
+ | --------------- | --------- | ----------- |
2702
+ | `login` | `string` | |
2703
+ | `id?` | `number` | |
2704
+ | `[key: string]` | `unknown` | |
2705
+
2706
+ ### `GitHubPullRequest`
2707
+
2708
+ | Field | Type | Description |
2709
+ | --------------- | ------------------------------------------------------------------------------------------------------------- | ----------- |
2710
+ | `number` | `number` | |
2711
+ | `draft?` | `boolean` | |
2712
+ | `title?` | `string` | |
2713
+ | `body?` | `string` | |
2714
+ | `state?` | `string` | |
2715
+ | `merged?` | `boolean` | |
2716
+ | `head` | `{ ref: string; sha: string; repo?: { full_name: string; [key: string]: unknown }; [key: string]: unknown; }` | |
2717
+ | `base` | `{ ref: string; repo?: { full_name: string; [key: string]: unknown }; [key: string]: unknown; }` | |
2718
+ | `user?` | `GitHubUser` | |
2719
+ | `labels?` | `Array<{ name: string; [key: string]: unknown }>` | |
2720
+ | `[key: string]` | `unknown` | |
2721
+
2722
+ ### `GitHubCommit`
2723
+
2724
+ | Field | Type | Description |
2725
+ | --------------- | ------------------------------------------------------------------------------ | ----------- |
2726
+ | `id` | `string` | |
2727
+ | `message` | `string` | |
2728
+ | `author?` | `{ name?: string; email?: string; username?: string; [key: string]: unknown }` | |
2729
+ | `timestamp?` | `string` | |
2730
+ | `added?` | `string[]` | |
2731
+ | `removed?` | `string[]` | |
2732
+ | `modified?` | `string[]` | |
2733
+ | `[key: string]` | `unknown` | |
2734
+
2735
+ ### `GitHubComment`
2736
+
2737
+ | Field | Type | Description |
2738
+ | --------------- | ------------ | ----------- |
2739
+ | `id` | `number` | |
2740
+ | `body` | `string` | |
2741
+ | `user` | `GitHubUser` | |
2742
+ | `[key: string]` | `unknown` | |
2743
+
2744
+ ### `GitHubReview`
2745
+
2746
+ | Field | Type | Description |
2747
+ | --------------- | ------------ | ----------- |
2748
+ | `id` | `number` | |
2749
+ | `state` | `string` | |
2750
+ | `body?` | `string` | |
2751
+ | `user` | `GitHubUser` | |
2752
+ | `[key: string]` | `unknown` | |
2753
+
2754
+ ### `GitHubRelease`
2755
+
2756
+ | Field | Type | Description |
2757
+ | ------------------- | --------- | ----------- |
2758
+ | `id` | `number` | |
2759
+ | `tag_name` | `string` | |
2760
+ | `name?` | `string` | |
2761
+ | `body?` | `string` | |
2762
+ | `draft?` | `boolean` | |
2763
+ | `prerelease?` | `boolean` | |
2764
+ | `target_commitish?` | `string` | |
2765
+ | `[key: string]` | `unknown` | |
2766
+
2767
+ ---
2768
+
2172
2769
  ## SDK reference: idempotent
2173
2770
 
2174
- Source: https://kici.dev/docs/user/sdk/idempotent/
2771
+ Source: https://docs.kici.dev/user/sdk/idempotent/
2175
2772
 
2176
2773
  The SDK exposes two idempotency helpers — a generic function `idempotent()` and a step factory `idempotentStep()` — for the common case where a workflow step should:
2177
2774
 
@@ -2321,7 +2918,7 @@ The caller never has to branch on outcome — `result.result` is always a `Bucke
2321
2918
 
2322
2919
  ## SDK reference: rules, matrix, dynamic jobs
2323
2920
 
2324
- Source: https://kici.dev/docs/user/sdk/rules-matrix-dynamic/
2921
+ Source: https://docs.kici.dev/user/sdk/rules-matrix-dynamic/
2325
2922
 
2326
2923
  ## Rules
2327
2924
 
@@ -2442,17 +3039,7 @@ You can also narrow directly with `if (ctx.event.type === 'pull_request')` — T
2442
3039
 
2443
3040
  `EventPayload` is a discriminated union over the `type` field. Each variant provides typed access to the normalized event fields and the raw webhook payload.
2444
3041
 
2445
- **Common fields** (on all variants via `EventBase`):
2446
-
2447
- | Field | Type | Description |
2448
- | ---------------- | ------------------------------ | --------------------------------------- |
2449
- | `type` | `string` (literal per variant) | Event type discriminant |
2450
- | `action` | `string \| undefined` | Sub-action (e.g. 'opened', 'created') |
2451
- | `targetBranch` | `string \| undefined` | Target branch (push target, PR base) |
2452
- | `sourceBranch` | `string \| undefined` | Source branch (PR head) |
2453
- | `provider` | `string \| undefined` | Provider name (e.g. 'github') |
2454
- | `senderUsername` | `string \| undefined` | Webhook sender username |
2455
- | `payload` | `Record<string, unknown>` | Raw webhook payload (typed per variant) |
3042
+ 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).
2456
3043
 
2457
3044
  **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`.
2458
3045
 
@@ -2480,16 +3067,16 @@ step('test', async ({ $, matrix }) => {
2480
3067
 
2481
3068
  ```typescript
2482
3069
  matrix: {
2483
- os: ['ubuntu-latest', 'macos-latest'],
3070
+ os: ['linux', 'arm64'],
2484
3071
  node: ['18', '20'],
2485
3072
  }
2486
3073
  ```
2487
3074
 
2488
- Creates 4 job instances (2 x 2). In steps, values are named properties:
3075
+ Creates 4 job instances (2 x 2). The `os` values (`linux`, `arm64`) are **customer-defined scaler labels** matched by subset semantics against the labels your orchestrator advertises in its scaler `labelSets` — not hosted-runner names. In steps, values are named properties:
2489
3076
 
2490
3077
  ```typescript
2491
3078
  step('test', async ({ $, matrix }) => {
2492
- console.log(matrix!.os); // 'ubuntu-latest' or 'macos-latest'
3079
+ console.log(matrix!.os); // 'linux' or 'arm64'
2493
3080
  console.log(matrix!.node); // '18' or '20'
2494
3081
  });
2495
3082
  ```
@@ -2522,14 +3109,14 @@ Fine-tune matrix combinations on multi-dimensional matrices:
2522
3109
 
2523
3110
  ```typescript
2524
3111
  matrix: {
2525
- os: ['ubuntu-latest', 'macos-latest', 'windows-latest'],
3112
+ os: ['linux', 'arm64', 'windows'],
2526
3113
  node: ['18', '20', '22'],
2527
3114
  },
2528
3115
  exclude: [
2529
- { os: 'windows-latest', node: '18' },
3116
+ { os: 'windows', node: '18' },
2530
3117
  ],
2531
3118
  include: [
2532
- { os: 'ubuntu-latest', node: '23' },
3119
+ { os: 'linux', node: '23' },
2533
3120
  ],
2534
3121
  ```
2535
3122
 
@@ -2599,7 +3186,7 @@ const discoverJobs: DynamicJobFn = async ({ $ }) => {
2599
3186
  const packages = result.stdout.trim().split('\n');
2600
3187
  return packages.map((pkg) =>
2601
3188
  job(`test-${pkg}`, {
2602
- runsOn: 'ubuntu-latest',
3189
+ runsOn: 'linux',
2603
3190
  steps: [
2604
3191
  step('test', async ({ $ }) => {
2605
3192
  await $`cd packages/${pkg} && pnpm test`;
@@ -2644,7 +3231,7 @@ for (const item of workflow.jobs) {
2644
3231
 
2645
3232
  ## SDK reference: runtime
2646
3233
 
2647
- Source: https://kici.dev/docs/user/sdk/runtime/
3234
+ Source: https://docs.kici.dev/user/sdk/runtime/
2648
3235
 
2649
3236
  ## Types
2650
3237
 
@@ -2858,7 +3445,7 @@ Use whichever style is convenient — you don't have to wrap `console.log` in th
2858
3445
 
2859
3446
  ### setEnv(key, value)
2860
3447
 
2861
- Set an environment variable that is visible to the current step and all subsequent steps in the same job. This is the equivalent of `echo "KEY=VALUE" >> $GITHUB_ENV` in GitHub Actions.
3448
+ Export an environment variable to later steps in the same job. This is the canonical way to hand a value computed in one step to the steps that follow the equivalent of `echo "KEY=VALUE" >> $GITHUB_ENV` in GitHub Actions. The value is visible to the current step and all subsequent steps in the job.
2862
3449
 
2863
3450
  ```typescript
2864
3451
  step('setup', async (ctx) => {
@@ -2879,6 +3466,7 @@ step('use', async (ctx) => {
2879
3466
  - Last-write-wins -- if multiple steps set the same key, the last value is used
2880
3467
  - Cannot override operator-injected secrets (the operator value takes precedence)
2881
3468
  - Changes take effect immediately in the current step and persist for all subsequent steps
3469
+ - 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)
2882
3470
 
2883
3471
  ### addPath(dir)
2884
3472
 
@@ -2896,6 +3484,40 @@ step('build', async (ctx) => {
2896
3484
  });
2897
3485
  ```
2898
3486
 
3487
+ ### Exporting env from shell commands ($KICI_ENV / $KICI_PATH)
3488
+
3489
+ `setEnv` and `addPath` are the TypeScript form of "export env to later steps". A shell command — including a non-JS toolchain installer — exports env the same way by appending to two files the agent points at before every step:
3490
+
3491
+ - **`$KICI_ENV`** — append `KEY=value` lines. Each becomes an environment variable visible to subsequent steps, exactly like `ctx.setEnv('KEY', 'value')`.
3492
+ - **`$KICI_PATH`** — append one directory per line. Each is prepended to `PATH` for subsequent steps, exactly like `ctx.addPath(dir)`. The first directory appended ends up first on `PATH`.
3493
+
3494
+ ```typescript
3495
+ step('install-tool', async (ctx) => {
3496
+ await ctx.$`./install-mytool.sh`; // installs to /opt/mytool
3497
+ // Export from the shell, no JS round-trip needed:
3498
+ await ctx.$`echo "MYTOOL_HOME=/opt/mytool" >> "$KICI_ENV"`;
3499
+ await ctx.$`echo "/opt/mytool/bin" >> "$KICI_PATH"`;
3500
+ });
3501
+
3502
+ step('build', async (ctx) => {
3503
+ // MYTOOL_HOME is set and /opt/mytool/bin is on PATH here.
3504
+ await ctx.$`mytool build`;
3505
+ });
3506
+ ```
3507
+
3508
+ **Format (v1):**
3509
+
3510
+ - One `KEY=value` per line in `$KICI_ENV`. The split is on the first `=`, so the value may contain `=`. Blank lines and lines without a `=` are ignored.
3511
+ - One directory per line in `$KICI_PATH`. Blank lines are ignored.
3512
+ - Values must be single-line — embedded newlines are not supported in v1.
3513
+
3514
+ **Behavior (shared with `setEnv` / `addPath`):**
3515
+
3516
+ - Applied after the step completes and visible to every later step in the job.
3517
+ - Last-write-wins on a repeated key.
3518
+ - Cannot override an operator-injected secret — a collision is ignored and logged, and the operator value is preserved.
3519
+ - The files are reset before each step, so each step sees only its own appended lines.
3520
+
2899
3521
  ### setSecretOutput(key, value)
2900
3522
 
2901
3523
  Publish an encrypted secret output from this job. Downstream jobs that list this job in their `needs` array receive the value merged into `ctx.secrets`.
@@ -2944,7 +3566,7 @@ Each job picks its secret environment via the `environment` option on `job()`. T
2944
3566
 
2945
3567
  ```typescript
2946
3568
  const deploy = job('deploy', {
2947
- runsOn: 'ubuntu-latest',
3569
+ runsOn: 'linux',
2948
3570
  environment: 'production',
2949
3571
  steps: [
2950
3572
  /* ... */
@@ -2983,7 +3605,7 @@ step('deploy', async ({ secrets }) => {
2983
3605
  import { workflow, job, step, push } from '@kici-dev/sdk';
2984
3606
 
2985
3607
  const deploy = job('deploy', {
2986
- runsOn: 'ubuntu-latest',
3608
+ runsOn: 'linux',
2987
3609
  environment: 'production',
2988
3610
  steps: [
2989
3611
  step('deploy', async (ctx) => {
@@ -3110,7 +3732,7 @@ Options can also be provided as an async factory function for dynamic fixture ge
3110
3732
 
3111
3733
  ## SDK reference: triggers
3112
3734
 
3113
- Source: https://kici.dev/docs/user/sdk/triggers/
3735
+ Source: https://docs.kici.dev/user/sdk/triggers/
3114
3736
 
3115
3737
  ## Triggers
3116
3738
 
@@ -3611,7 +4233,7 @@ Glob patterns use micromatch syntax. Regex patterns use standard JavaScript `Reg
3611
4233
 
3612
4234
  ## SDK reference: validation & events
3613
4235
 
3614
- Source: https://kici.dev/docs/user/sdk/validation-events/
4236
+ Source: https://docs.kici.dev/user/sdk/validation-events/
3615
4237
 
3616
4238
  ## Validation
3617
4239
 
@@ -3774,7 +4396,7 @@ The orchestrator automatically emits system events for workflow and job completi
3774
4396
 
3775
4397
  ## SDK reference: waitFor
3776
4398
 
3777
- Source: https://kici.dev/docs/user/sdk/wait-for/
4399
+ Source: https://docs.kici.dev/user/sdk/wait-for/
3778
4400
 
3779
4401
  The SDK exposes two wait-for helpers — a generic function `waitFor()` and a step factory `waitForStep()` — for the common case where a workflow step should:
3780
4402
 
@@ -3890,7 +4512,7 @@ If `check()` throws while polling, the error is logged and polling continues —
3890
4512
 
3891
4513
  ## SDK reference
3892
4514
 
3893
- Source: https://kici.dev/docs/user/sdk-reference/
4515
+ Source: https://docs.kici.dev/user/sdk-reference/
3894
4516
 
3895
4517
  Reference documentation for `@kici-dev/sdk`. The reference is split across five pages by topic.
3896
4518
 
@@ -3899,6 +4521,7 @@ Reference documentation for `@kici-dev/sdk`. The reference is split across five
3899
4521
  | [Core](./sdk/core.md) | `workflow()`, `job()`, `step()` factory functions and step / job authoring patterns (bare functions, output chaining, `needs`, dynamic groups). |
3900
4522
  | [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. |
3901
4523
  | [Rules, matrix, dynamic jobs](./sdk/rules-matrix-dynamic.md) | `rule()`, `skip()`, matrix builds (static + dynamic), and `dynamicJob()` / `dynamicGroup()`. |
4524
+ | [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. |
3902
4525
  | [Validation & events](./sdk/validation-events.md) | `validateDag()`, `defineEvent()`, event emission patterns. |
3903
4526
  | [Runtime](./sdk/runtime.md) | Types index, `StepContext`, secrets, and fixtures. |
3904
4527
  | [Idempotent helpers](./sdk/idempotent.md) | `idempotent()` and `idempotentStep()` — check / apply pattern with typed results on both the skipped and applied branches. |
@@ -3927,7 +4550,7 @@ For the complete list of every named export (factory functions, triggers, rules,
3927
4550
 
3928
4551
  ## CLI authentication
3929
4552
 
3930
- Source: https://kici.dev/docs/user/cli-auth/
4553
+ Source: https://docs.kici.dev/user/cli-auth/
3931
4554
 
3932
4555
  The KiCI CLI supports three authentication methods: browser-based OAuth (default), device authorization flow (for headless environments), and API key paste (for CI/CD pipelines).
3933
4556
 
@@ -4089,7 +4712,7 @@ JWT and opaque OIDC tokens are validated against the configured OIDC issuer (JWK
4089
4712
 
4090
4713
  ### Permissions
4091
4714
 
4092
- 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 14 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).
4715
+ 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).
4093
4716
 
4094
4717
  ### Configurable surfaces
4095
4718
 
@@ -4188,7 +4811,7 @@ If the CLI can't reach the server:
4188
4811
 
4189
4812
  ## CLI reference
4190
4813
 
4191
- Source: https://kici.dev/docs/user/cli-reference/
4814
+ Source: https://docs.kici.dev/user/cli-reference/
4192
4815
 
4193
4816
  The `@kici-dev/compiler` package provides the `kici` CLI for compiling, testing, and managing workflows.
4194
4817
 
@@ -4346,6 +4969,7 @@ Cleanup policy:
4346
4969
  - On a fully successful run, the isolated checkout is removed.
4347
4970
  - On failure, it is retained and its path is logged so you can inspect the failed state.
4348
4971
  - `--keep` always retains it, even on success.
4972
+ - Retained checkouts are garbage-collected after 72 hours by the next `kici run local` invocation — copy a checkout elsewhere if you need it longer.
4349
4973
 
4350
4974
  Set the `KICI_RUN_DIR` environment variable to place the isolated checkout under a base directory other than the system temp directory.
4351
4975
 
@@ -4402,6 +5026,8 @@ kici run local push --keep-going
4402
5026
 
4403
5027
  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.
4404
5028
 
5029
+ Requires `kici login` (an authenticated session) and a target orchestrator that has **cache storage configured** (`KICI_STORAGE_TYPE` = `s3` or `filesystem`) — the command uploads your working-tree overlay to that storage for the agent to fetch. The quickstart orchestrators do not enable storage by default; see the [testing guide](testing-guide.md) for setup (including non-public / self-hosted S3 endpoints).
5030
+
4405
5031
  ```bash
4406
5032
  kici run remote [fixture] [options]
4407
5033
  ```
@@ -4414,21 +5040,21 @@ kici run remote [fixture] [options]
4414
5040
 
4415
5041
  **Options:**
4416
5042
 
4417
- | Option | Default | Description |
4418
- | --------------------------- | ------- | -------------------------------------------------- |
4419
- | `--all` | `false` | Run all fixtures |
4420
- | `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
4421
- | `--parallel` | `false` | Run multiple fixtures concurrently |
4422
- | `--no-wait` | - | Fire and forget (print runIds, don't stream) |
4423
- | `--quiet` | `false` | Minimal output (only final result) |
4424
- | `--json` | `false` | Machine-readable JSON output |
4425
- | `--junit <path>` | none | JUnit XML output to file for CI integration |
4426
- | `--history` | `false` | Show table of recent test runs |
4427
- | `--routing-key <key>` | none | Override routing key for this run |
4428
- | `--secret <key=value>` | none | Inject flat secret (repeatable) |
4429
- | `--context <ctx.key=value>` | none | Inject context secret (repeatable) |
4430
- | `--debug` | `false` | Verbose internals |
4431
- | `--kici-dir <path>` | `.kici` | Path to .kici directory |
5043
+ | Option | Default | Description |
5044
+ | --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
5045
+ | `--all` | `false` | Run all fixtures |
5046
+ | `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
5047
+ | `--parallel` | `false` | Run multiple fixtures concurrently |
5048
+ | `--no-wait` | - | Fire and forget (print runIds, don't stream) |
5049
+ | `--quiet` | `false` | Minimal output (only final result) |
5050
+ | `--json` | `false` | Machine-readable JSON output |
5051
+ | `--junit <path>` | none | JUnit XML output to file for CI integration |
5052
+ | `--history` | `false` | Show table of recent test runs |
5053
+ | `--routing-key <key>` | none | Override routing key for this run |
5054
+ | `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted to the orchestrator (repeatable) |
5055
+ | `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted to the orchestrator (repeatable) — see [testing guide](testing-guide.md) |
5056
+ | `--debug` | `false` | Verbose internals |
5057
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
4432
5058
 
4433
5059
  **Examples:**
4434
5060
 
@@ -4561,15 +5187,14 @@ kici login [options]
4561
5187
 
4562
5188
  **Environment variables:**
4563
5189
 
4564
- | Variable | Default | Description |
4565
- | ---------------------- | ------------------------- | ---------------------------------------------------------------------- |
4566
- | `KICI_PLATFORM_URL` | none (required for OAuth) | Platform API base URL |
4567
- | `KICI_OIDC_ISSUER` | none (required for OAuth) | OIDC issuer URL |
4568
- | `KICI_OIDC_CLIENT_ID` | none (required for OAuth) | OIDC client ID |
4569
- | `KICI_OIDC_PROJECT_ID` | none | OIDC project ID |
4570
- | `KICI_BROWSER_CMD` | uses `open` package | Custom browser command with `{url}` placeholder, or `none` to suppress |
4571
- | `KICI_CALLBACK_PORT` | random | Fixed port for OAuth PKCE callback server |
4572
- | `KICI_CONFIG_DIR` | `~/.kici` | Override config directory |
5190
+ | Variable | Default | Description |
5191
+ | --------------------- | -------------------------------------------- | ---------------------------------------------------------------------- |
5192
+ | `KICI_PLATFORM_URL` | `https://api.kici.dev` | Platform API base URL (override for a self-hosted Platform) |
5193
+ | `KICI_OIDC_ISSUER` | `https://auth.kici.dev/realms/kici-internal` | OIDC issuer URL (override for a self-hosted Platform) |
5194
+ | `KICI_OIDC_CLIENT_ID` | `kici-cli` | OIDC client ID (override for a self-hosted Platform) |
5195
+ | `KICI_BROWSER_CMD` | uses `open` package | Custom browser command with `{url}` placeholder, or `none` to suppress |
5196
+ | `KICI_CALLBACK_PORT` | random | Fixed port for OAuth PKCE callback server |
5197
+ | `KICI_CONFIG_DIR` | `~/.kici` | Override config directory |
4573
5198
 
4574
5199
  **Examples:**
4575
5200
 
@@ -5162,7 +5787,7 @@ Set `KICI_DEBUG=true` for additional internal debug output across all commands.
5162
5787
 
5163
5788
  ## Lifecycle hooks
5164
5789
 
5165
- Source: https://kici.dev/docs/user/hooks/
5790
+ Source: https://docs.kici.dev/user/hooks/
5166
5791
 
5167
5792
  Hooks are callbacks that run at specific points in the execution lifecycle. They let you react to outcomes (cancellation, success, failure) and perform cleanup without affecting the execution flow.
5168
5793
 
@@ -5402,7 +6027,7 @@ _Source: `packages/sdk/src/hooks/`, `packages/sdk/src/types.ts`_
5402
6027
 
5403
6028
  ## Lock file and workflow drift
5404
6029
 
5405
- Source: https://kici.dev/docs/user/lock-file-and-drift/
6030
+ Source: https://docs.kici.dev/user/lock-file-and-drift/
5406
6031
 
5407
6032
  KiCI uses a **two-artifact model**: TypeScript workflows are the source of truth; the lock file (`kici.lock.json`) is the execution contract. The orchestrator reads only the lock file to match triggers and decide cache vs build. Keeping these in sync is important.
5408
6033
 
@@ -5417,13 +6042,13 @@ If you change a workflow file (`.ts`) but do **not** regenerate and commit the l
5417
6042
 
5418
6043
  The lock file (`kici.lock.json`) is a JSON file with the following top-level fields:
5419
6044
 
5420
- | Field | Description |
5421
- | --------------- | ------------------------------------------------------------------------------------------------------------------- |
5422
- | `schemaVersion` | Lock file schema version (currently 12). Incremented on breaking format changes. |
5423
- | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
5424
- | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
5425
- | `lockfileHash` | SHA-256 of `.kici/package-lock.json`, used as the dependency cache key. Omitted when no package lock file exists. |
5426
- | `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
6045
+ | Field | Description |
6046
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6047
+ | `schemaVersion` | Lock file schema version (currently 15). Incremented on breaking format changes. |
6048
+ | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
6049
+ | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
6050
+ | `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. |
6051
+ | `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
5427
6052
 
5428
6053
  Each workflow entry includes:
5429
6054
 
@@ -5439,7 +6064,11 @@ Each workflow entry includes:
5439
6064
  | `description` | Optional workflow description. |
5440
6065
  | `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). |
5441
6066
  | `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
6067
+ | `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
6068
+ | `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). |
6069
+ | `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). |
5442
6070
  | `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](concurrency.md). |
6071
+ | `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
5443
6072
  | Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
5444
6073
 
5445
6074
  ## Rule: commit both together
@@ -5525,7 +6154,7 @@ So even without a pre-commit or CI check, a stale lock file will cause the run t
5525
6154
 
5526
6155
  ## Testing guide
5527
6156
 
5528
- Source: https://kici.dev/docs/user/testing-guide/
6157
+ Source: https://docs.kici.dev/user/testing-guide/
5529
6158
 
5530
6159
  Test your workflows remotely against the full CI pipeline from your local machine. `kici run remote` uploads your current repo state (including uncommitted changes), triggers the pipeline, and streams execution logs back in real time.
5531
6160
 
@@ -5535,11 +6164,22 @@ Test your workflows remotely against the full CI pipeline from your local machin
5535
6164
 
5536
6165
  - Run any workflow against your current working tree (including unstaged changes)
5537
6166
  - Get real-time log output streamed back to your terminal
5538
- - Use test-specific secret contexts that are isolated from production
6167
+ - 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
5539
6168
  - Detect test mode in workflow code via `ctx.isTestRun`
5540
6169
 
5541
6170
  The command is remote-only -- all execution happens on the orchestrator and agent. For local-only trigger matching previews, use `kici test <event>`.
5542
6171
 
6172
+ :::note[Orchestrator prerequisite: cache storage]
6173
+ `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`).
6174
+
6175
+ - **The [Docker / Podman quickstart](quickstart/compose.md) wires this up for you** — it ships a SeaweedFS service, so `kici run remote` works there out of the box (see its "run a workflow without pushing" step).
6176
+ - **The [bare-metal quickstart](quickstart/bare-metal.md) does not configure storage by default** — enable a backend before using `kici run remote`:
6177
+ - **`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.
6178
+ - **`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.
6179
+
6180
+ See [Storage layout](../operator/orchestrator/storage-layout.md) for the full env-var reference.
6181
+ :::
6182
+
5543
6183
  ## Getting started
5544
6184
 
5545
6185
  ### 1. Authenticate
@@ -5638,7 +6278,7 @@ When not specified, these default to values detected from your local git repo (c
5638
6278
 
5639
6279
  ### Secret context mappings
5640
6280
 
5641
- Map test secret contexts to your fixture:
6281
+ Map secret contexts to your fixture:
5642
6282
 
5643
6283
  ```typescript
5644
6284
  export const pushWithSecrets = fixture('push-with-secrets', {
@@ -5650,7 +6290,12 @@ export const pushWithSecrets = fixture('push-with-secrets', {
5650
6290
  });
5651
6291
  ```
5652
6292
 
5653
- This maps the `db` secret context to the `test-database` context on the orchestrator, and `api` to `test-api-keys`. Only environments with `allowLocalExecution: true` are accessible. See [Secret contexts for testing](#secret-contexts-for-testing) below.
6293
+ This maps the `db` secret context to the `test-database` context, and `api` to `test-api-keys`.
6294
+
6295
+ This mapping is honored by **both** `kici run local` and `kici run remote`:
6296
+
6297
+ - 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).
6298
+ - 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).
5654
6299
 
5655
6300
  ### Async fixtures
5656
6301
 
@@ -5781,39 +6426,76 @@ The CLI always shows a pre-upload summary before transferring:
5781
6426
 
5782
6427
  ## Secret contexts for testing
5783
6428
 
5784
- Test runs can only access secret contexts that have been explicitly marked as test-accessible.
6429
+ The goal of the test-secret model is to let test runs reach **test-only credentials** while keeping production credentials out of reach. `kici run remote` combines two sources of secrets for a test run, then merges them with a clear precedence and a fail-closed gate.
6430
+
6431
+ ### CLI-uploaded local secrets
6432
+
6433
+ `kici run remote` collects the same local secret values that `kici run local` reads — `.kici/.secrets`, `.kici/.env.local`, `.kici/secrets.yaml`, and any `--env KEY=VALUE` flags — and uploads them **encrypted** to the orchestrator alongside the run. The orchestrator decrypts them only to inject them into the agent for that run; the control plane never sees the values.
5785
6434
 
5786
- ### Operator configuration
6435
+ ```bash
6436
+ # Provide an ad-hoc test value for a single remote run
6437
+ kici run remote push-main --env KICI_DATABASE_URL=postgresql://localhost/test
6438
+ ```
6439
+
6440
+ `--env` provides a **flat** per-run override; `--context <ctx>.<KEY>=<value>` is its sibling for a **namespaced** per-run override, placing the value under the named context `ctx`. Both are uploaded **encrypted** and follow the same precedence rule below — a CLI-supplied value wins over the orchestrator test-environment secret on a key collision.
6441
+
6442
+ ```bash
6443
+ # Provide a namespaced per-run value under the 'db' context
6444
+ kici run remote push-db --context db.KICI_DATABASE_URL=postgresql://localhost/test
6445
+ ```
5787
6446
 
5788
- Test-run access to a secret context is gated by a single flag on the environment that owns the bound secrets:
6447
+ Because these values originate on your machine, they are the natural place to put throwaway test credentials without touching any orchestrator-stored secret.
5789
6448
 
5790
- - **`allowLocalExecution`** -- When `true`, CLI-initiated test runs (`kici run remote`) may resolve secrets through this environment. Production environments should leave this `false` so test runs cannot reach production credentials.
6449
+ ### Orchestrator test-environment secrets
5791
6450
 
5792
- ### Fixture secret mappings
6451
+ In addition to your uploaded values, the orchestrator resolves test-scoped secrets from its own store for a remote test run:
5793
6452
 
5794
- In your fixture, map local names to remote context names:
6453
+ - 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.
6454
+ - Each fixture `secrets: { ctx: envName }` mapping resolves the named environment's secrets under the namespaced context `ctx`.
6455
+
6456
+ Both paths are restricted to environments flagged `allowLocalExecution: true`. A production environment left at the default `false` is never resolvable for a test run.
5795
6457
 
5796
6458
  ```typescript
5797
6459
  export const pushWithDb = fixture('push-db', {
5798
6460
  event: push({ branches: ['main'] }),
5799
- secrets: { db: 'test-database' },
6461
+ secrets: { db: 'test-database' }, // 'test-database' must be allowLocalExecution: true
5800
6462
  });
5801
6463
  ```
5802
6464
 
5803
- In your workflow, access secrets normally via `ctx.secrets` or `ctx.contexts`:
5804
-
5805
6465
  ```typescript
5806
6466
  step('migrate', async (ctx) => {
5807
- const dbUrl = ctx.secrets.KICI_DATABASE_URL;
6467
+ const dbUrl = await ctx.secrets.get('KICI_DATABASE_URL');
5808
6468
  await ctx.$`npx prisma migrate deploy`;
5809
6469
  });
5810
6470
  ```
5811
6471
 
5812
- During a test run, the `KICI_DATABASE_URL` secret is resolved from the `test-database` context instead of the production context.
6472
+ ### Precedence: CLI values win
6473
+
6474
+ When a key exists in both sources, the **CLI-uploaded local value wins** over the orchestrator test-environment value. This makes a local override a per-run knob: set `--env KICI_DATABASE_URL=...` (or put it in `.kici/.secrets`) to shadow the test environment's value for just that run, without changing anything on the orchestrator.
6475
+
6476
+ ### Fail-closed on non-test environments
6477
+
6478
+ Test-run secret resolution is fail-closed:
6479
+
6480
+ - If a fixture maps a context to an environment that does not exist, the run is **rejected**.
6481
+ - If a fixture maps a context to an environment whose `allowLocalExecution` is `false`, the run is **rejected**.
6482
+ - The `allowLocalExecution` gate applies to **all** remote test runs: a run whose matched workflow targets an environment with the flag off is rejected, so a test run can never resolve production secrets.
6483
+
6484
+ ### The `allowLocalExecution` environment flag
6485
+
6486
+ Each environment carries an `allowLocalExecution` flag (default `false`) that controls test-run access to that environment and to its secrets. Production environments should leave it at `false`; create a dedicated test environment with `allowLocalExecution: true` that binds only test-only secret scopes for the contexts you want test runs to use.
6487
+
6488
+ The flag is set by the orchestrator operator, either via the CLI:
6489
+
6490
+ ```bash
6491
+ kici-admin environment set-policy --env test-database --allow-local-execution true
6492
+ ```
6493
+
6494
+ or via the dashboard's "Test runs" toggle on the environment detail page. `kici secrets list` only surfaces contexts whose owning environment has `allowLocalExecution: true`, so production environments are never advertised as test-accessible.
5813
6495
 
5814
- ### Fail-closed behavior
6496
+ ### Local execution as an alternative
5815
6497
 
5816
- If a workflow accesses a secret context that has no test-allowed equivalent, the run fails immediately. This prevents accidental use of production secrets in test runs.
6498
+ `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.
5817
6499
 
5818
6500
  ### Discovering available contexts
5819
6501
 
@@ -5898,7 +6580,7 @@ export const pushMain = fixture('push-main', {
5898
6580
 
5899
6581
  ## Workflow patterns
5900
6582
 
5901
- Source: https://kici.dev/docs/user/workflow-patterns/
6583
+ Source: https://docs.kici.dev/user/workflow-patterns/
5902
6584
 
5903
6585
  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.
5904
6586
 
@@ -5925,7 +6607,7 @@ Practical patterns for building real-world KiCI workflows in TypeScript. The pat
5925
6607
 
5926
6608
  ## Concurrency groups
5927
6609
 
5928
- Source: https://kici.dev/docs/user/concurrency/
6610
+ Source: https://docs.kici.dev/user/concurrency/
5929
6611
 
5930
6612
  Concurrency groups prevent multiple workflow runs from executing in parallel when they target the same resource. Common use cases include preventing parallel deploys to the same environment or serializing database migrations.
5931
6613
 
@@ -5975,8 +6657,8 @@ group: (ctx) => `deploy-${ctx.branch}`;
5975
6657
  // Global concurrency (across all branches)
5976
6658
  group: () => 'deploy';
5977
6659
 
5978
- // Per-environment concurrency
5979
- group: (ctx) => `deploy-${ctx.event.environment ?? 'default'}`;
6660
+ // Per-target-branch concurrency
6661
+ group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
5980
6662
  ```
5981
6663
 
5982
6664
  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.
@@ -6150,10 +6832,34 @@ _Source: `packages/sdk/src/types.ts` (WorkflowOptions.concurrency, JobOptions.co
6150
6832
 
6151
6833
  ## Dashboard
6152
6834
 
6153
- Source: https://kici.dev/docs/user/dashboard/
6835
+ Source: https://docs.kici.dev/user/dashboard/
6154
6836
 
6155
6837
  The KiCI dashboard is a web-based interface for monitoring workflow runs, inspecting job and step details, and reading log output. It is a browser single-page application that authenticates via OIDC and communicates with the Platform tier through REST API endpoints.
6156
6838
 
6839
+ ## Getting started
6840
+
6841
+ <!-- help:getting-started-overview#getting-started -->
6842
+
6843
+ The getting-started page is a six-step checklist that takes you from zero to your first workflow run.
6844
+
6845
+ - **Self-checked steps** -- install the CLI, scaffold a workflow, and run it locally. These run on your own machine, so you tick them off yourself; the dashboard remembers your choices in the browser.
6846
+ - **Auto-detected steps** -- connect an orchestrator, add a webhook source, and trigger your first run. These tick automatically as the dashboard observes the matching activity in your organization.
6847
+
6848
+ Each step links to the relevant settings page or documentation. A progress bar tracks overall completion, and the sidebar entry shows a `done/total` badge until you finish or dismiss reminders.
6849
+
6850
+ <!-- /help:getting-started-overview -->
6851
+
6852
+ When you first sign in to a brand-new organization with no orchestrator, no webhook source, and no runs, the dashboard opens this page automatically. Once your organization has any activity, the run list becomes your landing page instead. The **Getting started** sidebar entry stays available so you can return to the checklist at any time.
6853
+
6854
+ The six steps are:
6855
+
6856
+ 1. **Install the kici CLI** -- `npm install -g kici`.
6857
+ 2. **Create a workflow** -- `kici init` scaffolds a `.kici/` directory in your repository.
6858
+ 3. **Run a workflow locally** -- `kici run local pr:open` executes a workflow on your machine with no orchestrator required.
6859
+ 4. **Connect an orchestrator** -- deploy an orchestrator and connect it with a join token from **Settings → Orchestrator keys**.
6860
+ 5. **Add a webhook source** -- register a source under **Settings → Sources** so pushes and pull requests trigger runs.
6861
+ 6. **Trigger your first run** -- push to your repository to produce your first run through the relay.
6862
+
6157
6863
  ## Navigation
6158
6864
 
6159
6865
  ### Sidebar
@@ -6161,6 +6867,7 @@ The KiCI dashboard is a web-based interface for monitoring workflow runs, inspec
6161
6867
  The left sidebar provides persistent navigation across all org-scoped pages:
6162
6868
 
6163
6869
  - **Org switcher** -- dropdown at the top to switch between organizations
6870
+ - **Getting started** -- onboarding checklist (shows a `done/total` badge until complete or dismissed)
6164
6871
  - **Runs** -- the default landing page, showing your workflow run history
6165
6872
  - **Workflows** -- permanently registered workflows listening for events
6166
6873
  - **Diagnostics** -- infrastructure health, execution metrics, and recent errors
@@ -6438,7 +7145,7 @@ The job tree supports keyboard navigation:
6438
7145
  The content area has the following tabs:
6439
7146
 
6440
7147
  - **Logs** (default) -- shows log output for the selected job or step
6441
- - **Payload** -- webhook payload viewer showing the raw event payload that triggered the run
7148
+ - **Payload** -- webhook payload viewer showing the raw event payload that triggered the run. This tab appears only for runs triggered by a webhook event (and re-runs of those, which copy the original payload); runs started by a schedule, manual schedule, lifecycle event, or another run carry no payload, so the tab is hidden for them
6442
7149
  - **Timeline** -- CSS Gantt chart showing the execution timeline of all jobs, with percentage-based bars and striped animation for running jobs. A **Provisioning** milestones section between the dispatch and execution phases plots scaler lifecycle events for the run — including a **Provisioning failed** marker when the scaler could not bring an agent up
6443
7150
  - **Summary** -- contextual overview scoped to the current selection (run-level trigger/repo/timing info, or job-level execution context with environment variables, runtime info, and sandbox details)
6444
7151
 
@@ -6553,7 +7260,7 @@ Each member's linked provider accounts (e.g. GitHub) are also visible here.
6553
7260
 
6554
7261
  <!-- help:settings-roles#settings -->
6555
7262
 
6556
- Roles define granular permissions across 14 resource categories (runs, secrets, members, etc.) with 5 access levels: `none`, `read`, `read_payload`, `write`, `admin`.
7263
+ Roles define granular permissions across 15 resource categories (runs, secrets, members, etc.) with 5 access levels: `none`, `read`, `read_payload`, `write`, `admin`.
6557
7264
 
6558
7265
  Create custom roles to restrict what team members can do, or use the built-in **Owner** role for full access.
6559
7266
 
@@ -6565,6 +7272,8 @@ API keys allow programmatic access to the KiCI API for automation, scripts, and
6565
7272
 
6566
7273
  Each key is scoped to this organization with a custom permission matrix and an optional expiry date. Keys can be revoked individually.
6567
7274
 
7275
+ Use a key's clone button to open the creation modal prefilled with that key's name, expiry, and permissions — handy for recreating an expired key or deriving a new key from an existing one.
7276
+
6568
7277
  <!-- /help:settings-api-keys -->
6569
7278
 
6570
7279
  <!-- help:settings-orchestrator-keys#orchestrator-keys -->
@@ -6573,6 +7282,8 @@ Orchestrator keys authenticate the WebSocket connection between your orchestrato
6573
7282
 
6574
7283
  Create a key here and set it as the `KICI_PLATFORM_TOKEN` environment variable in your orchestrator configuration. Keys can optionally be restricted to specific routing patterns.
6575
7284
 
7285
+ Use a key's clone button to open the creation modal prefilled with that key's name and description.
7286
+
6576
7287
  <!-- /help:settings-orchestrator-keys -->
6577
7288
 
6578
7289
  <!-- help:settings-sources#sources -->
@@ -6625,6 +7336,8 @@ The relayed-webhooks counter only includes webhooks delivered through the KiCI P
6625
7336
 
6626
7337
  Webhooks pointed directly at your orchestrator's public ingest endpoint never reach the Platform, so they're invisible to this counter and uncapped on every Hosted tier. If you have a public orchestrator ingress, you can mix-and-match: use the relay for sources you can't expose publicly, and point GitHub (or any provider / generic webhook) straight at your orchestrator for the rest.
6627
7338
 
7339
+ Every webhook the relay forwards counts — **including ones your workflows ultimately ignore**. Trigger matching runs on your orchestrator, not on the Platform, so the relay forwards each signature-verified webhook before any trigger is evaluated. A source that sends many events you filter down to a handful of runs still consumes one relayed webhook per event. If a high-volume source mostly produces no run, point it directly at your orchestrator (see above) to keep it off this counter entirely.
7340
+
6628
7341
  When you hit the cap, new relayed webhooks are rejected with `429 Plan limit reached`. Upgrade in the Stripe Billing Portal to lift the cap immediately; usage resets monthly on your billing anniversary.
6629
7342
 
6630
7343
  <!-- /help:settings-billing-relayed-webhooks -->
@@ -6794,6 +7507,20 @@ The summary strip at the top shows total / enabled / disabled counts plus whethe
6794
7507
 
6795
7508
  <!-- /help:settings-security-dashboard-policy -->
6796
7509
 
7510
+ <!-- help:settings-support-access#settings -->
7511
+
7512
+ Controls whether KiCI support staff may open read-only support sessions against your organization. Sessions are **off by default** — nobody outside your org can read your data until you opt in here.
7513
+
7514
+ When enabled:
7515
+
7516
+ - KiCI staff can open time-boxed, read-only sessions to investigate an issue.
7517
+ - Every read they perform is recorded in your audit trail with the support reason.
7518
+ - No writes are ever possible during a session.
7519
+
7520
+ Disabling the toggle immediately ends any in-progress support session. Only users with the `support:admin` permission (owners by default) can change this setting.
7521
+
7522
+ <!-- /help:settings-support-access -->
7523
+
6797
7524
  <!-- help:settings-webhooks-delivery-log#settings -->
6798
7525
 
6799
7526
  The delivery log shows recent webhook deliveries for an endpoint, including the HTTP status code, number of retry attempts, and the event payload.
@@ -6850,11 +7577,26 @@ The settings page (`/orgs/:customerId/settings`) uses a tabbed layout:
6850
7577
  10. **Webhooks** -- outbound webhook endpoint management with delivery logs and test ping
6851
7578
  11. **Event log** -- inbound webhook delivery log (visible with `event_log:read` permission)
6852
7579
  12. **Security** -- read-only view of the orchestrator's dashboard-write policy matrix (visible with `org_settings:read` permission)
7580
+ 13. **Support access** -- opt-in switch that controls whether KiCI support staff may open read-only support sessions against your org (visible with `support:read`; toggled with `support:admin`)
6853
7581
 
6854
7582
  Audit-log-style entries are no longer a settings tab; they live on the dedicated **Activity** page accessible from the sidebar.
6855
7583
 
6856
7584
  Tab selection syncs with the URL path (`/settings/members`, `/settings/api-keys`, etc.), making tabs bookmarkable.
6857
7585
 
7586
+ ### Support access
7587
+
7588
+ The Support access tab controls whether KiCI support staff may open a read-only **support session** against your organization to help diagnose an issue. The setting is **off by default** -- until you opt in here, no one outside your org can read your data.
7589
+
7590
+ When support access is enabled:
7591
+
7592
+ - A KiCI operator can open a time-boxed (30-minute, renewable), read-only support session scoped to a stated reason.
7593
+ - A support session is **runs-only**: the operator can browse your run list and, by confirming each run individually, view that run's detail and step logs. Nothing else is visible, and no write is ever possible.
7594
+ - Every run an operator opens is recorded in your [Activity](#activity) audit trail, attributed to the operator with the support reason -- so you can see exactly what was looked at and why.
7595
+
7596
+ **Disabling immediately ends any active session.** Toggling the switch off closes every in-progress support session for your org at once. Enabling and disabling the setting is itself audited, attributed to the user who changed it.
7597
+
7598
+ Viewing the setting requires the `support:read` permission; changing it requires `support:admin` (granted to owners by default).
7599
+
6858
7600
  ### Orchestrator keys
6859
7601
 
6860
7602
  The orchestrator keys tab manages API keys used to authenticate orchestrator-to-Platform WebSocket connections. These are separate from user API keys (which grant dashboard/API access).
@@ -6931,6 +7673,8 @@ Personal access tokens (PATs) are long-lived credentials for programmatic API ac
6931
7673
 
6932
7674
  Create a PAT to authenticate CLI tools or scripts without going through the OIDC login flow. Tokens can be revoked at any time.
6933
7675
 
7676
+ Use a token's clone button to open the creation modal prefilled with that token's name, expiry, and permissions.
7677
+
6934
7678
  <!-- /help:personal-pats -->
6935
7679
 
6936
7680
  <!-- help:personal-linked-accounts#account -->
@@ -7029,7 +7773,7 @@ The policy state is visible at three layers in the UI:
7029
7773
  - A **lock-icon prefix** on every disabled control, with a per-control CLI hint.
7030
7774
  - A **per-page banner** on any page containing at least one disabled operation, listing every disabled op on that page and its CLI equivalent.
7031
7775
 
7032
- The Security policy page (Settings → Security → Dashboard policy) renders the full 23-row read-only matrix with the current state and the `kici-admin` command for each row. The policy itself cannot be changed from the dashboard — the orchestrator operator manages it via `kici-admin org-settings dashboard-writes`. See [Dashboard-write policy](/operator/security/dashboard-write-policy) for the operator-side details.
7776
+ The Security policy page (Settings → Security → Dashboard policy) renders the full 24-row read-only matrix with the current state and the `kici-admin` command for each row. The policy itself cannot be changed from the dashboard — the orchestrator operator manages it via `kici-admin org-settings dashboard-writes`. See [Dashboard-write policy](/operator/security/dashboard-write-policy) for the operator-side details.
7033
7777
 
7034
7778
  ## Approval queue
7035
7779
 
@@ -7107,16 +7851,27 @@ The dashboard shows informative error pages instead of blank screens:
7107
7851
 
7108
7852
  ## Dynamic values
7109
7853
 
7110
- Source: https://kici.dev/docs/user/dynamic-values/
7854
+ Source: https://docs.kici.dev/user/dynamic-values/
7111
7855
 
7112
- Dynamic values let you compute `environment`, `env`, and `concurrencyGroup` at runtime based on the incoming event. Instead of hardcoding static strings, you pass a function that receives the webhook event payload and returns the resolved value.
7856
+ Dynamic values let you compute `environment`, `env`, and `concurrencyGroup` at runtime based on the incoming event. Instead of hardcoding static strings, you pass a function that receives the normalized event envelope and returns the resolved value.
7113
7857
 
7114
7858
  ```typescript
7115
7859
  job('deploy', {
7116
7860
  runsOn: ['default'],
7117
- environment: (event) => event.ref.split('/').pop(),
7118
- env: (event) => ({ BRANCH: event.ref.split('/').pop() }),
7119
- concurrencyGroup: (event) => `deploy-${event.ref.split('/').pop()}`,
7861
+ environment: (event) => event.targetBranch,
7862
+ env: (event) => ({ BRANCH: event.targetBranch }),
7863
+ concurrencyGroup: (event) => `deploy-${event.targetBranch}`,
7864
+ steps: [
7865
+ /* ... */
7866
+ ],
7867
+ });
7868
+ ```
7869
+
7870
+ ```typescript
7871
+ job('deploy', {
7872
+ runsOn: 'default',
7873
+ // One shape everywhere: branch on the normalized event type.
7874
+ environment: (event) => (event.type === 'pull_request' ? 'preview' : 'production'),
7120
7875
  steps: [
7121
7876
  /* ... */
7122
7877
  ],
@@ -7143,21 +7898,21 @@ When the compiler detects a pure function, it serializes the function source dir
7143
7898
  **Examples of pure functions:**
7144
7899
 
7145
7900
  ```typescript
7146
- // Simple branch extraction from ref
7147
- environment: (event) => event.ref.split('/').pop();
7901
+ // Simple branch extraction
7902
+ environment: (event) => event.targetBranch;
7148
7903
 
7149
7904
  // Object literal with string operations
7150
- env: ({ ref }) => ({ BRANCH: ref.split('/').pop() });
7905
+ env: (event) => ({ BRANCH: event.targetBranch });
7151
7906
 
7152
7907
  // Concatenation with event data
7153
- concurrencyGroup: (event) => `deploy-${event.ref.split('/').pop()}`;
7908
+ concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
7154
7909
 
7155
7910
  // Using safe globals
7156
- env: (event) => ({ UPPER: String(event.ref).toUpperCase() });
7911
+ env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
7157
7912
 
7158
7913
  // Local variables are fine
7159
7914
  environment: (event) => {
7160
- const parts = event.ref.split('/');
7915
+ const parts = event.targetBranch.split('/');
7161
7916
  return parts[parts.length - 1];
7162
7917
  };
7163
7918
  ```
@@ -7177,7 +7932,7 @@ This adds approximately 5-10 seconds of overhead for cloning and evaluation.
7177
7932
 
7178
7933
  ```typescript
7179
7934
  // Async functions cannot be inlined
7180
- environment: async (event) => await lookupEnv(event.ref);
7935
+ environment: async (event) => await lookupEnv(event.targetBranch);
7181
7936
 
7182
7937
  // External module references
7183
7938
  env: (event) => {
@@ -7197,35 +7952,35 @@ env: async (event) => {
7197
7952
 
7198
7953
  ## Performance comparison
7199
7954
 
7200
- | Evaluation path | Overhead | When used |
7201
- | ------------------------------------ | -------- | ---------------------------------------------------------- |
7202
- | Static value (string/object literal) | ~0ms | `environment: 'staging'` |
7203
- | Inline expression (pure function) | ~0ms | `environment: (event) => event.ref.split('/').pop()` |
7204
- | Init job (impure function) | ~5-10s | `environment: async (event) => await lookupEnv(event.ref)` |
7955
+ | Evaluation path | Overhead | When used |
7956
+ | ------------------------------------ | -------- | ------------------------------------------------------------------- |
7957
+ | Static value (string/object literal) | ~0ms | `environment: 'staging'` |
7958
+ | Inline expression (pure function) | ~0ms | `environment: (event) => event.targetBranch` |
7959
+ | Init job (impure function) | ~5-10s | `environment: async (event) => await lookupEnv(event.targetBranch)` |
7205
7960
 
7206
7961
  ## Tips
7207
7962
 
7208
7963
  - **Write pure functions whenever possible** to avoid the init-job delay. Most environment and env computations only need the event payload data.
7209
7964
  - **Check compiler warnings** -- the compiler tells you when a function is classified as impure and explains why.
7210
7965
  - **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.
7211
- - **The event parameter** contains the normalized webhook payload. For push events this includes `ref`, `repository`, `sender`, etc. For PR events it includes `pull_request`, `action`, etc.
7966
+ - **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`, …).
7212
7967
 
7213
7968
  ---
7214
7969
 
7215
7970
  ## Environment variables
7216
7971
 
7217
- Source: https://kici.dev/docs/user/env-vars/
7972
+ Source: https://docs.kici.dev/user/env-vars/
7218
7973
 
7219
- The KiCI CLI reads the following environment variables to customize its behavior. OAuth login (`kici login` without `--token`) requires `KICI_PLATFORM_URL`, `KICI_OIDC_ISSUER`, and `KICI_OIDC_CLIENT_ID` to be set the CLI does not ship defaults for them.
7974
+ The KiCI CLI reads the following environment variables to customize its behavior. OAuth login (`kici login` without `--token`) defaults `KICI_PLATFORM_URL`, `KICI_OIDC_ISSUER`, and `KICI_OIDC_CLIENT_ID` to the hosted KiCI Platform, so `kici login` works with no configuration. Set them only to target a self-hosted Platform or a testing environment.
7220
7975
 
7221
7976
  ## Authentication
7222
7977
 
7223
- | Variable | Description | Default |
7224
- | --------------------- | ----------------------------------------------------------- | ------------------ |
7225
- | `KICI_OIDC_ISSUER` | OIDC issuer URL for authentication (required for OAuth) | none — must be set |
7226
- | `KICI_OIDC_CLIENT_ID` | OIDC client ID for the CLI application (required for OAuth) | none — must be set |
7227
- | `KICI_PLATFORM_URL` | Platform API base URL (required for OAuth) | none — must be set |
7228
- | `KICI_CONFIG_DIR` | Override the KiCI config directory | `~/.kici` |
7978
+ | Variable | Description | Default |
7979
+ | --------------------- | -------------------------------------- | -------------------------------------------- |
7980
+ | `KICI_OIDC_ISSUER` | OIDC issuer URL for authentication | `https://auth.kici.dev/realms/kici-internal` |
7981
+ | `KICI_OIDC_CLIENT_ID` | OIDC client ID for the CLI application | `kici-cli` |
7982
+ | `KICI_PLATFORM_URL` | Platform API base URL | `https://api.kici.dev` |
7983
+ | `KICI_CONFIG_DIR` | Override the KiCI config directory | `~/.kici` |
7229
7984
 
7230
7985
  ## Browser behavior
7231
7986
 
@@ -7251,9 +8006,9 @@ Authenticate with a pre-existing API key (no browser needed):
7251
8006
  kici login --token <<< "$KICI_API_KEY"
7252
8007
  ```
7253
8008
 
7254
- ### Custom OIDC provider
8009
+ ### Self-hosted Platform or custom OIDC provider
7255
8010
 
7256
- Point the CLI to a self-hosted or testing OIDC provider:
8011
+ `kici login` targets the hosted KiCI Platform by default. To point the CLI at a self-hosted Platform or a testing OIDC provider, override the defaults:
7257
8012
 
7258
8013
  ```bash
7259
8014
  export KICI_OIDC_ISSUER=https://your-idp.example.com
@@ -7295,7 +8050,7 @@ kici login
7295
8050
 
7296
8051
  ## Environments
7297
8052
 
7298
- Source: https://kici.dev/docs/user/environments/
8053
+ Source: https://docs.kici.dev/user/environments/
7299
8054
 
7300
8055
  <!-- help:environments-list#overview -->
7301
8056
 
@@ -7360,12 +8115,12 @@ export default workflow('deploy', {
7360
8115
 
7361
8116
  ### Dynamic environments
7362
8117
 
7363
- The environment name can be a string or a function (sync or async) for dynamic environments (e.g., per-PR review environments):
8118
+ The environment name can be a string or a function (sync or async) for dynamic environments (e.g., per-PR review environments). The function receives the normalized event envelope, with the raw provider body nested at `event.payload`:
7364
8119
 
7365
8120
  ```typescript
7366
8121
  job('deploy-review', {
7367
8122
  runsOn: 'default',
7368
- environment: async (event) => `review/PR-${event.pull_request.number}`,
8123
+ environment: (event) => `review/PR-${event.payload.pull_request.number}`,
7369
8124
  steps: [
7370
8125
  step('deploy', async (ctx) => {
7371
8126
  // ctx.environment is 'review/PR-123' (resolved at runtime)
@@ -7375,7 +8130,7 @@ job('deploy-review', {
7375
8130
  });
7376
8131
  ```
7377
8132
 
7378
- Dynamic environments that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
8133
+ 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.
7379
8134
 
7380
8135
  ### Job-level environment variables
7381
8136
 
@@ -7387,7 +8142,7 @@ job('deploy', {
7387
8142
  environment: 'production',
7388
8143
  env: { DEPLOY_TARGET: 'us-east-1' },
7389
8144
  // Or dynamic:
7390
- // env: async (event) => ({ DEPLOY_SHA: event.after?.slice(0, 7) }),
8145
+ // env: (event) => ({ DEPLOY_SHA: event.payload.after?.slice(0, 7) }),
7391
8146
  steps: [
7392
8147
  step('deploy', async (ctx) => {
7393
8148
  // DEPLOY_TARGET is available in ctx.env
@@ -7409,7 +8164,7 @@ job('deploy', {
7409
8164
  environment: 'production',
7410
8165
  concurrencyGroup: 'production-api',
7411
8166
  // Or dynamic:
7412
- // concurrencyGroup: async (event) => `review-${event.pull_request.number}`,
8167
+ // concurrencyGroup: (event) => `review-${event.payload.pull_request.number}`,
7413
8168
  steps: [
7414
8169
  /* ... */
7415
8170
  ],
@@ -7536,10 +8291,12 @@ Strategy: queue (or cancel-pending)
7536
8291
 
7537
8292
  ### Creating environments
7538
8293
 
7539
- Navigate to **Settings > Environments** in the dashboard. Click **+ New** to create an inline row with the environment name and type (Fixed or Glob).
8294
+ Navigate to **Settings > Environments** in the dashboard. Click **New environment** to choose the environment name and type (Fixed or Glob).
8295
+
8296
+ - **Fixed** -- applies to jobs that declare exactly this environment name, like `staging` or `production`
8297
+ - **Glob** -- applies to any environment name a job declares that matches the pattern, e.g. `review/*` matches a job with `environment: 'review/PR-123'`
7540
8298
 
7541
- - **Fixed** -- a named environment like `staging` or `production`
7542
- - **Glob** -- a pattern like `review/*` that matches dynamic environment names
8299
+ 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.
7543
8300
 
7544
8301
  ### Environment detail page
7545
8302
 
@@ -7584,7 +8341,7 @@ interface EnvironmentSecrets {
7584
8341
 
7585
8342
  ## Event system
7586
8343
 
7587
- Source: https://kici.dev/docs/user/events/
8344
+ Source: https://docs.kici.dev/user/events/
7588
8345
 
7589
8346
  KiCI supports two broad categories of workflow triggers: **git-based triggers** that work immediately, and **event-based triggers** that use a registration model. Understanding this distinction is key to working effectively with non-git triggers like schedules, custom events, and generic webhooks.
7590
8347
 
@@ -7780,7 +8537,7 @@ export default workflow('nightly-build', {
7780
8537
  on: schedule({ cron: '0 2 * * *' }),
7781
8538
  jobs: [
7782
8539
  job('build', {
7783
- runsOn: 'ubuntu-latest',
8540
+ runsOn: 'linux',
7784
8541
  steps: [
7785
8542
  step('build', async ({ $ }) => {
7786
8543
  await $`pnpm build`;
@@ -7879,7 +8636,7 @@ export default workflow('build', {
7879
8636
  on: push({ branches: 'main' }),
7880
8637
  jobs: [
7881
8638
  job('build', {
7882
- runsOn: 'ubuntu-latest',
8639
+ runsOn: 'linux',
7883
8640
  steps: [
7884
8641
  step('build', async ({ $ }) => {
7885
8642
  await $`pnpm build`;
@@ -7934,7 +8691,7 @@ export default workflow('post-deploy', {
7934
8691
  on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }),
7935
8692
  jobs: [
7936
8693
  job('smoke-test', {
7937
- runsOn: 'ubuntu-latest',
8694
+ runsOn: 'linux',
7938
8695
  steps: [
7939
8696
  step('test', async ({ $ }) => {
7940
8697
  await $`./scripts/smoke-test.sh`;
@@ -7959,7 +8716,7 @@ Custom events are delivered immediately when emitted (mid-workflow, not queued u
7959
8716
 
7960
8717
  ## Global workflows
7961
8718
 
7962
- Source: https://kici.dev/docs/user/global-workflows/
8719
+ Source: https://docs.kici.dev/user/global-workflows/
7963
8720
 
7964
8721
  Global workflows let one **workflow repo** define jobs that run on events from many **source repos** in the same org. They're the answer to "I want one CI policy / release pipeline / security scan to fire on every repo without copy-pasting `.kici/` folders everywhere."
7965
8722
 
@@ -8081,7 +8838,7 @@ Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workf
8081
8838
 
8082
8839
  ## Private npm registries
8083
8840
 
8084
- Source: https://kici.dev/docs/user/private-registries/
8841
+ Source: https://docs.kici.dev/user/private-registries/
8085
8842
 
8086
8843
  A workflow's `.kici/package.json` may depend on packages published to a private registry — your org's internal CodeArtifact, a GitHub Packages scope, a self-hosted Verdaccio, JFrog, Cloudsmith, GitLab, etc. KiCI ships two ways to authenticate `npm install` against those registries from inside a job, plus an escape hatch for short-lived tokens.
8087
8844
 
@@ -8222,7 +8979,7 @@ export default workflow('build', {
8222
8979
  });
8223
8980
  ```
8224
8981
 
8225
- The same pattern works for GCP Artifact Registry — replace the `aws codeartifact` call with `gcloud auth print-access-token`. A typed `RegistryProvider` for these short-lived flows is on the roadmap; the manual setup-step is the supported path until then.
8982
+ The same pattern works for GCP Artifact Registry — replace the `aws codeartifact` call with `gcloud auth print-access-token`. The manual setup-step shown here is the supported path for these short-lived flows.
8226
8983
 
8227
8984
  ## Provider-specific examples
8228
8985
 
@@ -8329,7 +9086,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
8329
9086
 
8330
9087
  ## Secrets
8331
9088
 
8332
- Source: https://kici.dev/docs/user/secrets/
9089
+ Source: https://docs.kici.dev/user/secrets/
8333
9090
 
8334
9091
  KiCI provides an explicit secrets API that gives workflow steps controlled access to secrets stored in the orchestrator's secret store. Secrets are never auto-injected into `process.env` -- you must explicitly request each secret by name.
8335
9092
 
@@ -8595,7 +9352,7 @@ See [CLI reference](/user/cli) for the `kici types` command.
8595
9352
 
8596
9353
  ## GitHub App provider
8597
9354
 
8598
- Source: https://kici.dev/docs/user/providers/github/
9355
+ Source: https://docs.kici.dev/user/providers/github/
8599
9356
 
8600
9357
  The **GitHub App** is KiCI's flagship source. A single App:
8601
9358
 
@@ -8750,7 +9507,7 @@ kici-admin source get-webhook-secret github:12345 # Fetch the secret (for debug
8750
9507
  ```
8751
9508
 
8752
9509
  For the full CLI reference see the `source` section of the
8753
- [kici-admin CLI reference](../../operator/kici-admin-cli.md).
9510
+ [kici-admin CLI reference](../../operator/orchestrator/kici-admin-cli.md).
8754
9511
 
8755
9512
  ## Routing keys
8756
9513
 
@@ -8903,7 +9660,7 @@ and confirm the App is installed on that repo.
8903
9660
  Gogs / GitLab, and for plain-GitHub repos without an App
8904
9661
  - [GitHub checks architecture](../../architecture/webhooks/github-checks.md)
8905
9662
  - [Global workflows](../../architecture/global-workflows.md)
8906
- - [kici-admin CLI reference](../../operator/kici-admin-cli.md)
9663
+ - [kici-admin CLI reference](../../operator/orchestrator/kici-admin-cli.md)
8907
9664
  - [Event routing](../../operator/event-routing.md) — operator-level
8908
9665
  routing-key mechanics
8909
9666
 
@@ -8911,7 +9668,7 @@ and confirm the App is installed on that repo.
8911
9668
 
8912
9669
  ## Universal-git provider
8913
9670
 
8914
- Source: https://kici.dev/docs/user/providers/universal-git/
9671
+ Source: https://docs.kici.dev/user/providers/universal-git/
8915
9672
 
8916
9673
  The **universal-git** provider lets KiCI treat any git forge that speaks a
8917
9674
  GitHub-shaped webhook payload as a first-class source. That covers Forgejo,
@@ -9152,11 +9909,11 @@ with the right PEM.
9152
9909
 
9153
9910
  ## Data flows
9154
9911
 
9155
- Source: https://kici.dev/docs/architecture/data-flows/
9912
+ Source: https://docs.kici.dev/architecture/data-flows/
9156
9913
 
9157
9914
  This document describes the key data flows through the KiCI architecture: webhook delivery, job execution, dependency caching, re-run and cancel, trace ID propagation, internal event routing, and generic webhook ingestion.
9158
9915
 
9159
- > **Lock file schema version:** The lock file uses schema version 12, which adds `registries` and `installEnv` fields for private registry support on top of 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.
9916
+ > **Lock file schema version:** The lock file uses schema version 15, which adds per-job init config on top of v14's declarative cache specs, v11's `LockInlineValue` for pure function inline evaluation, v10's simplified negative patterns (! prefix in repos/paths arrays), v9's global workflow repos matching, and v8's runsOn polymorphic type support.
9160
9917
 
9161
9918
  ## Webhook delivery flow
9162
9919
 
@@ -9182,7 +9939,7 @@ GitHub --> Platform Relay --> Orchestrator --> Agent
9182
9939
  10. **Orchestrator fetches lock file** via the provider's `LockFileFetcher` (cached with LRU). For untrusted PR events, fetches both base and head lock files in parallel; for trusted PRs and pushes, fetches from head SHA.
9183
9940
  11. **Orchestrator detects workflow modifications** for untrusted PR events by comparing base and head lock files via `detectWorkflowModifications()`, applying security holds when non-trusted contributors modify workflow files.
9184
9941
  12. **Orchestrator extracts registrations** on default-branch pushes: persists registerable workflows (event, schedule, lifecycle triggers) for cluster-wide event matching.
9185
- 13. **Orchestrator registers event subscriptions** from lock file (if event routing is active) via `eventRouter.registerSubscriptions()`.
9942
+ 13. **Orchestrator notifies the event router** on default-branch pushes: after the registrations are persisted, emits a `registration.updated` event via `eventRouter.emit()` (if event routing is active). Workflow event subscriptions are the persisted registrations themselves, matched at emit time through the registration index.
9186
9943
  14. **Orchestrator fetches changed files** via the provider's `ChangedFilesFetcher` for path-based trigger filtering (skipped when no workflow uses path filters).
9187
9944
  15. **Orchestrator matches triggers** against lock file using `matchAllWorkflows()` from `@kici-dev/engine`.
9188
9945
  16. **Orchestrator checks caches** for source tarballs and dependency tarballs.
@@ -9325,9 +10082,9 @@ The source cache and dep cache are independent. Four combinations are possible:
9325
10082
 
9326
10083
  Dep cache misses alone do **not** trigger a build job. Deps are platform-specific (`deps/{platform}-{arch}/{hash}.tar.gz`) so a build job would need a builder agent matching the target platform, which may not exist (e.g., an arm64 builder when only x64 builders are available). When the source cache misses, the dispatched build job piggy-backs dep packing if deps are also missing. A single build job handles both artifacts when both miss, avoiding duplicate builds.
9327
10084
 
9328
- ### Legacy / cross-source / no-contentHash workflows
10085
+ ### Cross-source / no-contentHash workflows
9329
10086
 
9330
- - **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 12 (see [Design decisions](design-decisions.md#lock-file-schema-versioning)).
10087
+ - **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 15.
9331
10088
  - **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.
9332
10089
 
9333
10090
  ### Build deduplication
@@ -9411,6 +10168,64 @@ The two-phase metadata approach (`upload via PUT` then `initMeta via CopyObject`
9411
10168
 
9412
10169
  Agents receive pre-signed S3 GET URLs (15-minute expiry) directly in `job.dispatch` messages. Agents download artifacts from S3, bypassing the orchestrator for all data transfer.
9413
10170
 
10171
+ ## User-facing cache flow
10172
+
10173
+ 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.
10174
+
10175
+ 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.
10176
+
10177
+ ### Restore flow
10178
+
10179
+ ```
10180
+ Agent Orchestrator (UserCache) Object storage
10181
+ | | |
10182
+ |-- cache.user.restore.request ------->| |
10183
+ | { key, restoreKeys? } |-- exact key in read prefixes ->|
10184
+ | | (isolated: iso/<runId>/ |
10185
+ | | then shared/; trusted: |
10186
+ | | shared/ only) |
10187
+ | |-- restoreKeys prefix scan ---->|
10188
+ | | (newest match wins) |
10189
+ | |-- getUrl(matched) + touch ---->|
10190
+ |<-- cache.user.restore.response ------| |
10191
+ | { hit, matchedKey?, | |
10192
+ | downloadUrl?, tarHash? } | |
10193
+ | | |
10194
+ |-- HTTP GET (tarball body) -------------------------------------->|
10195
+ | (direct download; verify tarHash, extract paths) |
10196
+ ```
10197
+
10198
+ The restore resolves the exact `key` across the ref's read prefixes first, then each `restoreKeys` prefix in order (newest matching entry wins). A trusted ref reads only `shared/`; an untrusted/fork ref reads its own `iso/<runId>/` scope and then falls back to `shared/`. On a hit the response carries a presigned GET URL plus the tarball's `tarHash`, which the agent verifies before extracting.
10199
+
10200
+ ### Save flow (two-phase atomic)
10201
+
10202
+ ```
10203
+ Agent Orchestrator (UserCache) Object storage
10204
+ | | |
10205
+ |-- cache.user.save.request --------->| |
10206
+ | { key } |-- has(final key)? ------------>|
10207
+ | | (immutable: skip if exists) |
10208
+ | |-- getUploadUrl(.tmp-<uuid>) -->|
10209
+ |<-- cache.user.save.response ---------| |
10210
+ | { uploadUrl?, skip } | |
10211
+ | | |
10212
+ |-- HTTP PUT (tarball body) ----------------------------------->|
10213
+ | (direct upload to temp object) |
10214
+ | | |
10215
+ |-- cache.user.save.complete -------->| |
10216
+ | { key, tarHash, sizeBytes } |-- copy(temp -> final) -------->|
10217
+ | |-- delete(temp) --------------->|
10218
+ | |-- initMeta(final) ------------>|
10219
+ | |-- put(.hash) + put(.size) ---->|
10220
+ | |-- enforce per-org quota ------>|
10221
+ ```
10222
+
10223
+ The save is **immutable** and **atomic**. The orchestrator declines (`skip: true`) up front if the exact key already exists. Otherwise the agent uploads to a `.tmp-<uuid>` object via a presigned PUT, then `cache.user.save.complete` triggers a server-side copy temp→final, a delete of the temp, an `initMeta` to stamp TTL metadata, and `.hash` / `.size` companion writes. Because the final key only appears after the copy, a crashed upload never leaves a corrupt committed entry. The committing save then enforces the per-org byte quota, evicting oldest entries until the org is back under `KICI_USER_CACHE_QUOTA_BYTES`.
10224
+
10225
+ ### Trust → scope mapping
10226
+
10227
+ 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.
10228
+
9414
10229
  ## Internal event routing flow
9415
10230
 
9416
10231
  Internal events (custom events from `ctx.emit()` and system events from workflow/job completion) flow through the event router for fan-out delivery to matching workflows.
@@ -9443,11 +10258,11 @@ EventRouter.onNotification(eventId) [private]
9443
10258
  | increments attempts and records claimed_at/claimed_by atomically)
9444
10259
  |-- If lease acquired:
9445
10260
  | |-- processSubscriptions(event):
9446
- | | |-- If RegistrationIndex available (new path):
10261
+ | | |-- If RegistrationIndex available:
9447
10262
  | | | Look up registrations by trigger type
9448
10263
  | | | TrustStore.isTrusted() (for cross-repo events)
9449
10264
  | | | matchAllWorkflows() against registered workflows
9450
- | | |-- Else (legacy fallback):
10265
+ | | |-- Else (no RegistrationIndex):
9451
10266
  | | | TrustStore.isTrusted() (for cross-routing-key events)
9452
10267
  | | | matchAllWorkflows() against in-memory lock file subscriptions
9453
10268
  | | |-- For each match: onEventMatched(event, lockFile, matchedWorkflows)
@@ -9479,7 +10294,7 @@ Two invariants keep events from being silently lost:
9479
10294
  event lands in the DLQ (`dlq_at` set, `dlq_reason='exhausted_retries'`)
9480
10295
  and is surfaced via Prometheus (`kici_orch_event_dlq_*`), Grafana
9481
10296
  (`event-delivery` dashboard), and the kici-admin CLI
9482
- (`kici-admin event-dlq {list,retry,discard}`).
10297
+ (`kici-admin event-dlq {list,count,retry,discard}`).
9483
10298
  - **Crash detection:** when a node crashes mid-dispatch, its lease ages out
9484
10299
  after `leaseDurationMs` (default 60 s). The leader's
9485
10300
  `EventRetryScanner` releases the expired lease and re-publishes
@@ -9915,144 +10730,9 @@ The Platform tier exposes a `/ws/browser` WebSocket endpoint for dashboard clien
9915
10730
 
9916
10731
  ---
9917
10732
 
9918
- ## Design decisions
9919
-
9920
- Source: https://kici.dev/docs/architecture/design-decisions/
9921
-
9922
- This page explains the rationale behind KiCI's major architectural choices. Each section describes the decision, why it was made, and what alternatives were considered.
9923
-
9924
- ## TypeScript over YAML
9925
-
9926
- **Decision:** Workflows are defined in TypeScript, not YAML.
9927
-
9928
- TypeScript provides full language power -- type checking, IDE autocomplete, refactoring tools, conditional logic, and composability. Workflows are regular TypeScript code that can be tested, imported, and reused like any other module. YAML-based CI systems (GitHub Actions, CircleCI) limit expressiveness to what the YAML schema supports, requiring workarounds for anything beyond basic conditionals.
9929
-
9930
- **Alternative:** YAML workflow definitions (industry standard). Rejected because YAML constrains what users can express and makes workflows harder to test and refactor.
9931
-
9932
- ## Lock file approach
9933
-
9934
- **Decision:** The compiler generates a `kici.lock.json` file that separates workflow definitions from runtime execution.
9935
-
9936
- The lock file is a JSON snapshot of workflow definitions with static triggers pre-evaluated at compile time. This enables the orchestrator to match triggers without cloning code -- it fetches only the lock file via the GitHub API. Dynamic elements (dynamic jobs, dynamic matrices, rules) are represented as source references in the lock file and evaluated at runtime by the agent.
9937
-
9938
- **Alternative:** Runtime evaluation of TypeScript at the routing tier. Rejected because it would require the orchestrator to clone repositories for trigger matching, adding latency and complexity to the dispatch path.
9939
-
9940
- > See `packages/compiler/src/` for the compiler that generates lock files.
9941
-
9942
- ## Three-tier relay model
9943
-
9944
- **Decision:** The system is split into three deployment tiers: Platform (webhook router), orchestrator (execution brain), and agent (execution worker).
9945
-
9946
- This architecture separates trust boundaries. Customer code never leaves customer infrastructure -- the Platform tier handles only webhook verification and routing, with no access to customer code or secrets. The orchestrator matches triggers without cloning code. Only the agent, running on customer infrastructure, clones repositories and executes steps. The three-tier model also enables fully self-hosted deployment where all tiers run on customer infrastructure.
9947
-
9948
- **Alternative:** Monolithic Platform that runs everything (customer code on KiCI servers). Rejected because it requires customers to trust a third party with their source code and secrets.
9949
-
9950
- > See [Architecture Overview](overview.md) for the full three-tier diagram.
9951
-
9952
- ## Synchronous webhook relay
9953
-
9954
- **Decision:** The Platform tier relays webhooks synchronously over WebSocket: verify signature, route to orchestrator, wait for ACK, respond to GitHub.
9955
-
9956
- Synchronous relay is simpler, lower latency, and eliminates infrastructure dependencies. GitHub's built-in webhook retry mechanism handles the failure case -- if the Platform tier cannot relay (no orchestrator connected), it returns an error and GitHub retries automatically. This approach removed the need for a separate broker and an async job queue.
9957
-
9958
- **Alternative:** Async queue (store webhook in a queue, process later). Rejected because it added infrastructure complexity (a separate broker and queue layer) without meaningful benefit -- GitHub already provides reliable retry behavior.
9959
-
9960
- ## Per-source webhook secrets
9961
-
9962
- **Decision:** Each webhook source has its own webhook secret stored on the orchestrator side, dynamically registered by orchestrators via the `source.register` protocol message.
9963
-
9964
- Per-source secrets enable multi-tenant Platform with proper isolation. Each customer's webhook source (e.g., a GitHub App) has a unique webhook secret, and signature verification uses the correct secret for each incoming webhook based on the routing key derived from the `X-GitHub-Hook-Installation-Target-ID` header.
9965
-
9966
- **Alternative:** A single shared webhook secret via environment variable. Rejected because it cannot support multiple tenants and provides no isolation between customers.
9967
-
9968
- ## Routing key routing
9969
-
9970
- **Decision:** Webhooks are routed by provider-scoped routing key (e.g., `github:12345`) derived from the `X-GitHub-Hook-Installation-Target-ID` header. One routing key maps to one orchestrator connection.
9971
-
9972
- App ID routing provides a clean 1:1 mapping between a customer's GitHub App and their orchestrator. The orchestrator extracts the `installation_id` from the webhook payload body for GitHub API calls (fetching lock files, posting check runs).
9973
-
9974
- **Alternative:** Route by Installation ID (more granular, one App can have many installations). Rejected because it adds complexity without clear benefit -- most customers use one App with one or a few installations, and the orchestrator can handle multiple installations internally.
9975
-
9976
- ## PostgreSQL as the orchestrator's only data store
9977
-
9978
- **Decision:** The customer-deployable orchestrator depends on a single PostgreSQL database — no separate broker, cache, or pub/sub system. PostgreSQL is the primary data store on the Platform tier as well (the two tiers run independent databases).
9979
-
9980
- PostgreSQL backs the orchestrator's durable job queue (delayed and recurring jobs, retries, dead-letter handling), workflow runs, dispatched jobs, sources, secrets, and event registrations. All DB access goes through a typed SQL query layer. Cross-instance coordination inside an orchestrator cluster (peer announcements, run cancellation, secret invalidation) uses PostgreSQL LISTEN/NOTIFY, so adding orchestrator replicas does not introduce a new infrastructure dependency.
9981
-
9982
- **Alternative:** A separate broker / queue / pub-sub system alongside PostgreSQL. Rejected for the customer-deployable orchestrator because PostgreSQL covers durable jobs, transactional state changes, and cross-instance coordination in a single dependency — adding a broker would multiply operator surface area without buying customers a capability they don't already have.
9983
-
9984
- ## Pure-function state machine
9985
-
9986
- **Decision:** The state machine uses pure functions: `transition(state, event) -> newState` with no internal state, no classes, and no mutation.
9987
-
9988
- Pure functions are trivially testable (input/output, no setup), serializable (state is a string), and usable across all tiers without instantiation. The transition table is a plain `Record` for type-safe dispatch. The state machine tracks workflow runs, jobs, and steps using 11 states and 16 events.
9989
-
9990
- **Alternative:** Class-based state machine with internal state (e.g., `machine.apply(event)`). Rejected because it is harder to test (requires instantiation and lifecycle management), harder to serialize (class instances), and adds unnecessary complexity for a simple state transition problem.
9991
-
9992
- > See `packages/engine/src/state-machine/machine.ts` for the implementation and [State Machine](./execution/state-machine.md) for full documentation.
9993
-
9994
- ## Single agent binary with label-based routing
9995
-
9996
- **Decision:** One agent binary, configured with labels via the `KICI_LABELS` environment variable (e.g., `linux,docker,gpu`). The orchestrator routes jobs by matching `runsOn` labels against registered agent labels.
9997
-
9998
- A single binary simplifies deployment and maintenance. Labels provide flexible routing without requiring separate agent builds or deployments per platform. New capabilities are added by deploying the same binary with different labels and environment configuration.
9999
-
10000
- **Alternative:** Separate agent binaries per platform (e.g., `kici-agent-linux`, `kici-agent-docker`). Rejected because it multiplies build and deployment complexity without meaningful benefit.
10001
-
10002
- > See `packages/agent/src/config.ts` for label configuration.
10003
-
10004
- ## Lock file schema versioning
10005
-
10006
- **Decision:** The lock file includes a `schemaVersion` field that tracks breaking changes to the trigger format.
10007
-
10008
- | Version | Description |
10009
- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10010
- | 12 | Current schema. Covers everything in v11 plus per-workflow `registries` and `installEnv` fields for private-package authentication. Token bytes never appear in the lock file — only qualified `<environment>:<secret-name>` references that the orchestrator resolves at dispatch time through the per-environment secret resolver. |
10011
- | 11 | Covers the full trigger taxonomy (git events, internal events, schedule, lifecycle, generic webhooks), the `_type` discriminator on each trigger entry, polymorphic `runsOn` matching with `excludeLabels`, hook and concurrency flags on `LockJob`, repo-pattern matching with `!` negation, and `LockInlineValue` for pure-function inline evaluation. |
10012
-
10013
- The `_type` field on each lock file trigger entry identifies the trigger type for the matcher's exhaustive switch dispatch. Users must recompile workflows with `pnpm kici compile` after upgrading to get the latest trigger types in their lock files.
10014
-
10015
- **`SimulatedEvent.type`** is an open `string` type, allowing the `kici test` CLI to simulate any event type without requiring engine changes for each new event.
10016
-
10017
- ## Two-phase evaluation
10018
-
10019
- **Decision:** Trigger matching happens in two phases: fast reject at the orchestrator (no clone needed) and full evaluation at the agent (clone and run TypeScript).
10020
-
10021
- The orchestrator evaluates static triggers from the lock file -- branch patterns, path patterns, and event types. This is fast and requires only a small JSON file fetched via the GitHub API. The agent handles everything that requires code: rule evaluation (arbitrary TypeScript functions), dynamic job generation, and dynamic matrix expansion. This separation keeps the orchestrator fast and git-agnostic.
10022
-
10023
- **Alternative:** Full evaluation at the orchestrator (clone repo, run TypeScript, then dispatch). Rejected because it would make the orchestrator slow, require git access at the routing tier, and blur the trust boundary between routing and execution.
10024
-
10025
- ## Registration model for event-triggered workflows
10026
-
10027
- **Decision:** Workflows with non-Git triggers (schedule, kiciEvent, genericWebhook, workflowComplete, jobComplete, lifecycle) are extracted from the lock file on default-branch push and stored in a registration database. The orchestrator matches incoming events against these registrations without fetching the lock file each time.
10028
-
10029
- **Why:** Git-based triggers (push, PR) work with per-event lock file fetching because the webhook itself provides the repo and ref. Internal events (cron fires, custom events, workflow completions) do not carry repo/ref information -- the orchestrator needs to know which workflows to evaluate before the event arrives. The registration model solves this by pre-populating the orchestrator's knowledge of event-triggered workflows.
10030
-
10031
- **Alternative:** Fetch all lock files from all known repos on every internal event. Rejected because it would be O(repos \* events) API calls, adding unacceptable latency and GitHub API rate limit pressure.
10032
-
10033
- > See `packages/orchestrator/src/registration/` for the implementation.
10034
-
10035
- ## Event trigger matching via registration index
10036
-
10037
- **Decision:** The event router matches events exclusively against the DB-backed `RegistrationIndex`. A previous legacy in-memory subscription fallback was removed since the registration index is always available and is the sole authoritative source for trigger matching.
10038
-
10039
- **Why:** A single code path eliminates drift risk between dual paths, halves the test surface, and avoids confusing future contributors about which path is authoritative. The project's pre-release "no backward compatibility" rule means deferred cleanup shouldn't accumulate.
10040
-
10041
- > See `packages/orchestrator/src/events/event-router.ts` and `packages/orchestrator/src/registration/` for the implementation.
10042
-
10043
- ## See also
10044
-
10045
- - [Architecture Overview](overview.md) -- the three-tier model and package structure
10046
- - [State Machine](./execution/state-machine.md) -- details of the pure-function state machine
10047
- - [Webhook Delivery](./webhooks/webhook-delivery.md) -- synchronous relay in action
10048
- - [Reconnection](./clustering/reconnection.md) -- WebSocket resilience patterns
10049
- - [Event System Internals](./webhooks/event-system.md) -- event router, registration model, cron scheduler
10050
-
10051
- ---
10052
-
10053
10733
  ## Architecture overview
10054
10734
 
10055
- Source: https://kici.dev/docs/architecture/overview/
10735
+ Source: https://docs.kici.dev/architecture/overview/
10056
10736
 
10057
10737
  KiCI uses a three-tier relay model that separates webhook routing from code execution. Customer code never leaves customer infrastructure -- the Platform tier handles only webhook verification and routing, while the orchestrator and agent tiers run on customer-managed servers.
10058
10738
 
@@ -10137,7 +10817,7 @@ Shared business logic used by all three tiers. Single source of truth for cross-
10137
10817
  - Audit policy and retention (per-action access-log sampling, warm-retention windows for cold-store eligibility, federated activity row schema)
10138
10818
  - Scaler backend type enum (`container`, `bare-metal`, `firecracker`, `kubernetes`)
10139
10819
  - Registration trigger type enum (registerable trigger discriminator)
10140
- - Bundler config (shared bundler configuration consumed by `e2e/helpers/service-deploy.ts`; the agent runtime uses the shared `@kici-dev/shared/ts-loader-hook` to transform TypeScript on import, with no runtime bundler step)
10820
+ - Bundler config (shared bundler configuration consumed by `e2e/helpers/service-deploy.ts`; the agent runtime uses the `@kici-dev/core/ts-loader-hook` to transform TypeScript on import, with no runtime bundler step)
10141
10821
 
10142
10822
  > Source: `packages/engine/src/`
10143
10823
 
@@ -10153,9 +10833,15 @@ CLI tooling for workflow authors. Compiles `.kici/workflows/*.ts` to `.kici/kici
10153
10833
 
10154
10834
  > Source: `packages/compiler/src/`
10155
10835
 
10836
+ ### `@kici-dev/core`
10837
+
10838
+ Light shared utilities with no server-side dependencies — JSON-structured logging, error helpers, human-readable formatting (`formatBytes`/`formatDuration`/`formatUptime`), cryptographic helpers (`sha256`/`sha256File`/`deriveSharedSecret`), zx initialization (`initZx()`), and the TypeScript loader hook that transforms TypeScript on import. It is the dependency-light core that the SDK, compiler, and `kici` CLI consume directly so they stay free of heavier server-only dependencies. `@kici-dev/shared` re-exports it, so existing `@kici-dev/shared` import paths keep working.
10839
+
10840
+ > Source: `packages/core/src/`
10841
+
10156
10842
  ### `@kici-dev/shared`
10157
10843
 
10158
- Shared utilities used across packages. Provides `initZx()` for zx initialization, `createLogger()` for JSON-structured logging with TTY-aware formatting, `createPool()`/`createDb()` for typed PostgreSQL connections, `createMetricsRoutes()`/`createHealthRoutes()` for HTTP route factories (Prometheus metrics and health endpoints), `RingBuffer` for bounded collections, `requestContext`/`getRequestContext()`/`enrichRequestContext()` for async local storage request context, `getReconnectDelay()` for exponential backoff, `formatBytes`/`formatDuration`/`formatUptime` for human-readable formatting, `sha256`/`sha256File`/`deriveSharedSecret` for cryptographic utilities, `initTelemetry`/`createMeter` for OpenTelemetry integration, and `setupGracefulShutdown` for coordinated service shutdown with ordered steps.
10844
+ Shared utilities used across packages, including everything from `@kici-dev/core` (re-exported) plus server-side helpers. Provides `initZx()` for zx initialization, `createLogger()` for JSON-structured logging with TTY-aware formatting, `createPool()`/`createDb()` for typed PostgreSQL connections, `createMetricsRoutes()`/`createHealthRoutes()` for HTTP route factories (Prometheus metrics and health endpoints), `RingBuffer` for bounded collections, `requestContext`/`getRequestContext()`/`enrichRequestContext()` for async local storage request context, `getReconnectDelay()` for exponential backoff, `formatBytes`/`formatDuration`/`formatUptime` for human-readable formatting, `sha256`/`sha256File`/`deriveSharedSecret` for cryptographic utilities, `initTelemetry`/`createMeter` for OpenTelemetry integration, and `setupGracefulShutdown` for coordinated service shutdown with ordered steps.
10159
10845
 
10160
10846
  > Source: `packages/shared/src/`
10161
10847
 
@@ -10181,6 +10867,7 @@ The following diagram shows how `@kici` packages depend on each other. Solid arr
10181
10867
 
10182
10868
  ```mermaid
10183
10869
  flowchart TD
10870
+ CORE["@kici-dev/core"]
10184
10871
  SDK["@kici-dev/sdk"]
10185
10872
  COMPILER["@kici-dev/compiler"]
10186
10873
  SHARED["@kici-dev/shared"]
@@ -10192,26 +10879,30 @@ flowchart TD
10192
10879
 
10193
10880
  DASH --> ENGINE
10194
10881
  DASH -.->|dev| PLATFORM
10882
+ SHARED --> CORE
10195
10883
  SDK --> ENGINE
10196
- SDK --> SHARED
10884
+ SDK --> CORE
10197
10885
  COMPILER --> ENGINE
10198
- COMPILER --> SHARED
10886
+ COMPILER --> CORE
10199
10887
  COMPILER -.->|peer| SDK
10200
10888
  PLATFORM --> ENGINE
10201
10889
  PLATFORM --> SHARED
10202
10890
  ORCH --> ENGINE
10203
10891
  ORCH --> SHARED
10892
+ ORCH -.->|dev| AGENT
10204
10893
  AGENT --> ENGINE
10205
10894
  AGENT --> SDK
10206
10895
  AGENT --> SHARED
10896
+ AGENT --> CORE
10207
10897
  KICI["kici (wrapper)"]
10208
10898
  KICI --> COMPILER
10209
- KICI --> SHARED
10899
+ KICI --> CORE
10210
10900
  KICIADMIN["kici-admin (admin CLI)"]
10211
10901
  KICIADMIN --> ORCH
10902
+ KICIADMIN --> AGENT
10212
10903
  ```
10213
10904
 
10214
- **Leaf packages** (no `@kici` dependencies): `@kici-dev/shared` and `@kici-dev/engine`. These can be tested and built independently. The dashboard depends on `@kici-dev/engine` for shared types (protocol schemas, state machine) and imports the Platform's API type definitions as a dev dependency, but communicates with backend services at runtime via HTTP/WebSocket, not at compile time.
10905
+ **Leaf packages** (no `@kici` dependencies): `@kici-dev/core` and `@kici-dev/engine`. These can be tested and built independently. `@kici-dev/shared` builds on `@kici-dev/core` and re-exports it. The dashboard depends on `@kici-dev/engine` for shared types (protocol schemas, state machine) and imports the Platform's API type definitions as a dev dependency, but communicates with backend services at runtime via HTTP/WebSocket, not at compile time.
10215
10906
 
10216
10907
  **Runtime tiers** (Platform, orchestrator, agent) all depend on `@kici-dev/engine` for shared business logic and `@kici-dev/shared` for utilities. Only the agent depends on `@kici-dev/sdk` (it loads workflow definitions at runtime).
10217
10908
 
@@ -10245,6 +10936,5 @@ KiCI uses application-level tenant isolation. The Platform dashboard API accepts
10245
10936
  - [State Machine](./execution/state-machine.md) -- execution lifecycle tracking across all tiers
10246
10937
  - [Protocol Messages](protocol-messages.md) -- WebSocket message schemas for all three layers
10247
10938
  - [Webhook Delivery](./webhooks/webhook-delivery.md) -- end-to-end trace of a webhook through all three tiers
10248
- - [Design Decisions](design-decisions.md) -- rationale behind the three-tier model and other architectural choices
10249
10939
 
10250
10940
  ---