@kici-dev/compiler 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/cli.js +37 -7
  2. package/dist/commands/approve.d.ts +12 -0
  3. package/dist/commands/approve.js +5 -2
  4. package/dist/commands/compile.js +5 -15
  5. package/dist/commands/doctor.js +2 -2
  6. package/dist/commands/endpoints.js +4 -6
  7. package/dist/commands/held-run-client.d.ts +21 -1
  8. package/dist/commands/held-run-client.js +34 -15
  9. package/dist/commands/hook.js +22 -20
  10. package/dist/commands/index.d.ts +2 -0
  11. package/dist/commands/index.js +2 -1
  12. package/dist/commands/init.d.ts +9 -2
  13. package/dist/commands/init.js +43 -16
  14. package/dist/commands/login.js +1 -1
  15. package/dist/commands/orchestrators.js +3 -2
  16. package/dist/commands/preview.js +1 -8
  17. package/dist/commands/reject.d.ts +12 -0
  18. package/dist/commands/reject.js +5 -2
  19. package/dist/commands/report/collect.d.ts +82 -0
  20. package/dist/commands/report/collect.js +234 -0
  21. package/dist/commands/report/identity.d.ts +48 -0
  22. package/dist/commands/report/identity.js +49 -0
  23. package/dist/commands/report/index.d.ts +63 -0
  24. package/dist/commands/report/index.js +119 -0
  25. package/dist/commands/report/upload.d.ts +38 -0
  26. package/dist/commands/report/upload.js +64 -0
  27. package/dist/commands/run-hold-watch.js +2 -2
  28. package/dist/commands/run.js +6 -3
  29. package/dist/commands/runs/show.js +80 -1
  30. package/dist/commands/types.js +51 -9
  31. package/dist/errors/formatter.d.ts +2 -4
  32. package/dist/errors/formatter.js +1 -3
  33. package/dist/errors/index.d.ts +1 -1
  34. package/dist/errors/index.js +2 -2
  35. package/dist/execution/sdk-alias.js +4 -2
  36. package/dist/fixtures/compiler.js +2 -1
  37. package/dist/format.js +3 -3
  38. package/dist/generators/secrets-dts.d.ts +8 -3
  39. package/dist/generators/secrets-dts.js +3 -3
  40. package/dist/hooks/installer.js +2 -1
  41. package/dist/llm-context/llms-architecture.txt +59 -23
  42. package/dist/llm-context/llms-cli.txt +168 -34
  43. package/dist/llm-context/llms-features-execution.txt +2017 -0
  44. package/dist/llm-context/llms-features.txt +96 -1100
  45. package/dist/llm-context/llms-full.txt +3131 -1407
  46. package/dist/llm-context/llms-getting-started.txt +164 -31
  47. package/dist/llm-context/llms-patterns.txt +186 -7
  48. package/dist/llm-context/llms-providers.txt +15 -33
  49. package/dist/llm-context/llms-sdk-runtime.txt +62 -40
  50. package/dist/llm-context/llms-sdk.txt +284 -58
  51. package/dist/llm-context/llms.txt +23 -15
  52. package/dist/local-plane/paths.d.ts +15 -0
  53. package/dist/local-plane/paths.js +22 -1
  54. package/dist/local-plane/plane-manager.js +2 -2
  55. package/dist/local-plane/port-holder.js +1 -1
  56. package/dist/local-plane/postgres.d.ts +3 -16
  57. package/dist/local-plane/postgres.js +10 -15
  58. package/dist/lockfile/generator.d.ts +12 -0
  59. package/dist/lockfile/generator.js +182 -54
  60. package/dist/lockfile/index.d.ts +0 -2
  61. package/dist/lockfile/index.js +1 -2
  62. package/dist/postinstall.js +2 -1
  63. package/dist/remote/config.d.ts +2 -15
  64. package/dist/remote/config.js +2 -16
  65. package/dist/remote/dashboard-client.d.ts +39 -0
  66. package/dist/remote/dashboard-client.js +41 -0
  67. package/dist/remote/oauth.js +7 -5
  68. package/dist/remote/uploader.js +2 -2
  69. package/dist/templates/package-json.js +1 -1
  70. package/dist/test-runner/dry-run.d.ts +1 -2
  71. package/dist/test-runner/dry-run.js +5 -20
  72. package/dist/test-runner/git-detector.js +2 -1
  73. package/dist/test-runner/job-executor.js +2 -1
  74. package/dist/test-runner/payload-builder.js +11 -17
  75. package/dist/types.d.ts +64 -10
  76. package/dist/types.js +7 -1
  77. package/dist/validation/validator.js +63 -6
  78. package/package.json +16 -11
  79. package/sbom.spdx.json +957 -900
  80. package/dist/lockfile/purity-analyzer.d.ts +0 -25
  81. package/dist/lockfile/purity-analyzer.js +0 -204
  82. package/dist/lockfile/purity-diagnostics.d.ts +0 -31
  83. package/dist/lockfile/purity-diagnostics.js +0 -52
@@ -1,6 +1,6 @@
1
- # KiCI Workflow features
1
+ # KiCI Workflow features: access and approval
2
2
 
3
- This bundle covers: Workflow features: concurrency, contexts, secrets, approvals, provenance, events.
3
+ This bundle covers: Deployment contexts, scoped secrets, private registries, approvals, provenance, dashboard and account access.
4
4
 
5
5
  ## Account and sign-in
6
6
 
@@ -196,6 +196,10 @@ Here `build-plan` runs, then the job pauses for a `dba` approval. On approval, `
196
196
 
197
197
  Because a step-level hold keeps an agent and its workspace occupied for the whole human wait, prefer job- or workflow-level gates when you do not need prior-step state, and keep step-level timeouts short. See the [operator note on agent occupancy](https://docs.kici.dev/operator/approvals/#agent-occupancy-during-step-level-holds).
198
198
 
199
+ ### Not available on organization-wide workflows
200
+
201
+ An `approval` gate applies to per-repository workflows only. A workflow whose trigger carries `repos:` — an [organization-wide workflow](https://docs.kici.dev/user/global-workflows/) — is dispatched by a path that never consults the gate, so the gate would not hold anything. `kici compile` refuses it with `error [E124]` at the workflow level and on any static job, rather than accepting a security control the workflow does not have. Drop the `approval`, or move the gated jobs into a workflow whose triggers carry no `repos:`. A job produced by a `dynamicJob` generator never passes through the compiler, so that one is caught at dispatch instead: the orchestrator logs an error naming the workflow and job, and runs it ungated.
202
+
199
203
  ## Drift gates (`when: 'drift'`)
200
204
 
201
205
  A `when: 'drift'` gate is **step-scope only** and requires a [check/apply step](https://docs.kici.dev/user/idempotent-steps/). Instead of pausing unconditionally, it fires **between the step's `check` and `run`, only when `check` finds drift in apply mode** — exactly Terraform's plan→apply, scoped to one step. When the step is already in sync (no drift), nothing pauses and the step skips.
@@ -228,7 +232,7 @@ A `when: 'drift'` gate on a step without a `check` facet, or at job/workflow sco
228
232
 
229
233
  ## Mandatory vs. explicit gates
230
234
 
231
- `approval` is the **explicit** gate — a deliberate "pause for a human here" written by the workflow author. It composes with the **mandatory** gate an operator can attach to a protected context via required reviewers (see [Contexts](https://docs.kici.dev/user/contexts/#required-reviewers)). When both apply to the same job, all clauses from both sources must be satisfied before the job is released. The two funnel into one held-element mechanism, so the dashboard queue and `kici approve` work the same way regardless of which source held the element.
235
+ `approval` is the **explicit** gate — a deliberate "pause for a human here" written by the workflow author. It composes with the **mandatory** gate an operator can attach to a protected context via required reviewers (see [Contexts](https://docs.kici.dev/user/contexts/#required-reviewers)). When both apply to the same job, all clauses from both sources must be satisfied before the job is released. A workflow-level `approval` is one of those sources. It gates every root job, so a root job that also binds a context with required reviewers must satisfy the workflow clauses, its own job clauses, and the context's reviewers. The sources funnel into one held-element mechanism, so the dashboard queue and `kici approve` work the same way regardless of which source held the element.
232
236
 
233
237
  ## Approving from the CLI
234
238
 
@@ -250,6 +254,23 @@ kici reject <run-id> --job deploy-production --reason "Wrong release branch"
250
254
 
251
255
  You must be eligible for at least one unsatisfied clause — being a member of a named team or being a named user. The orchestrator verifies eligibility against the operator-defined teams, so naming a team in your workflow can never let an ineligible person release the gate. The command reports whether the element was released, how many clauses remain, or that it was rejected. See [`kici approve`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-approve) for the full command reference.
252
256
 
257
+ ### When a job is held twice
258
+
259
+ A job can carry an `approval` gate **and** a [security hold](https://docs.kici.dev/user/contexts/#security-approval-queue) at the same time. Two setups reach it: a fork pull request on an organization whose fork policy is `hold`, and a context that sets both required reviewers and `minimumTrust`.
260
+
261
+ The two are separate holds and **both** must be released before the job runs. They take different permissions: the approval hold needs `contexts:write` plus clause eligibility, the security hold needs `ci_trust:write`.
262
+
263
+ `--job` names both at once, so pick one with `--hold-type`:
264
+
265
+ ```bash
266
+ kici approve <run-id> --job deploy --hold-type reviewer
267
+ kici approve <run-id> --job deploy --hold-type security
268
+ ```
269
+
270
+ Pass `--hold <id>` when the command's error lists two holds that `--hold-type` still cannot separate. The pull request's `KiCI Security` check stays pending until both holds have ended, and its description names the second gate and the permission that clears it.
271
+
272
+ Each hold carries its own expiry, so the job is cancelled when the **first** one runs out.
273
+
253
274
  ### Inline approval and `--approve-all` in `kici run remote`
254
275
 
255
276
  When you trigger a run with `kici run remote` and it pauses on a gate, the CLI prints the gate (and, for a drift gate, the computed drift) and — in an interactive terminal — prompts you to approve or reject right there, without leaving the watch. In a non-interactive shell it prints how to approve out of band and keeps watching.
@@ -272,217 +293,7 @@ You can also approve from the dashboard approval queue. See [Dashboard](https://
272
293
  - [Contexts](https://docs.kici.dev/user/contexts/) — operator-required reviewers on protected contexts.
273
294
  - [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) — teams, the approval queue, expiry, and self-approval.
274
295
  - [Approval gates (architecture)](https://docs.kici.dev/architecture/approvals/) — the unified hold model and the step-level round-trip.
275
-
276
- ---
277
-
278
- ## Concurrency groups
279
-
280
- Source: https://docs.kici.dev/user/concurrency/
281
-
282
- 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.
283
-
284
- ## Basic usage
285
-
286
- ```typescript
287
- import { workflow, job, step, push } from '@kici-dev/sdk';
288
-
289
- export default workflow('deploy', {
290
- on: push({ branches: ['main', 'staging'] }),
291
- concurrency: {
292
- group: (ctx) => `deploy-${ctx.branch}`,
293
- cancelInProgress: true,
294
- max: 1,
295
- },
296
- jobs: [
297
- job('deploy', {
298
- runsOn: 'linux',
299
- steps: [
300
- step('deploy', async ({ $ }) => {
301
- await $`./deploy.sh`;
302
- }),
303
- ],
304
- }),
305
- ],
306
- });
307
- ```
308
-
309
- ## Configuration
310
-
311
- The `concurrency` option on a workflow accepts:
312
-
313
- | Field | Type | Default | Description |
314
- | ------------------ | -------- | -------- | ------------------------------------------ |
315
- | `group` | Function | Required | Returns the concurrency group key string |
316
- | `cancelInProgress` | boolean | `true` | Cancel older runs when a newer run arrives |
317
- | `max` | number | `1` | Maximum concurrent runs in the same group |
318
-
319
- ### Group key function
320
-
321
- The group key function receives a context with the branch name and event payload. Runs with the same group key are subject to concurrency limits.
322
-
323
- ```typescript
324
- // Per-branch concurrency (most common)
325
- group: (ctx) => `deploy-${ctx.branch}`;
326
-
327
- // Global concurrency (across all branches)
328
- group: () => 'deploy';
329
-
330
- // Per-target-branch concurrency
331
- group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
332
- ```
333
-
334
- The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. This differs from job-level `concurrencyGroup` (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)), where the compiler performs purity analysis and can inline pure functions for orchestrator-side evaluation.
335
-
336
- ## cancelInProgress mode
337
-
338
- When `cancelInProgress: true`, a newer run supersedes older runs in the same group:
339
-
340
- ```
341
- Run #1 starts deploying to main -> running
342
- Run #2 arrives for deploy-main group -> Run #1 cancelled ("Superseded by run in concurrency group 'deploy-main'")
343
- Run #2 continues -> running
344
- ```
345
-
346
- This is the most common mode for deploy workflows -- you want the latest code deployed, not an outdated version.
347
-
348
- The cancelled run:
349
-
350
- - Receives a cancellation with reason "Superseded by run in concurrency group 'deploy-main'"
351
- - Goes through the normal cancel flow (grace period, hooks if graceful)
352
- - GitHub Check status updated to `cancelled` with the superseded reason
353
-
354
- ```typescript
355
- workflow('deploy', {
356
- concurrency: {
357
- group: (ctx) => `deploy-${ctx.branch}`,
358
- cancelInProgress: true,
359
- },
360
- jobs: [/* ... */],
361
- });
362
- ```
363
-
364
- ## Queue mode
365
-
366
- When `cancelInProgress: false`, newer runs will wait until older runs complete:
367
-
368
- ```
369
- Run #1 starts deploying -> running
370
- Run #2 arrives for same group -> queued ("Waiting for deploy-main (1 ahead)")
371
- Run #1 completes -> success
372
- Run #2 starts -> running
373
- ```
374
-
375
- In queue mode, the agent that picked up the queued run **stays connected** to the orchestrator and parks on a long-poll wait. When the holder finishes (success, failure, or cancel), the orchestrator dequeues the FIFO-next entry and pushes a `proceed` notification over the same WebSocket; the queued agent then continues with normal step execution against the workspace it already has. The agent's slot is therefore held for the duration of the queue wait — bound by `KICI_CONCURRENCY_WAIT_TIMEOUT_MS` (default 1 hour).
376
-
377
- ```typescript
378
- workflow('migrate-db', {
379
- concurrency: {
380
- group: () => 'migrations',
381
- cancelInProgress: false,
382
- max: 1,
383
- },
384
- jobs: [/* ... */],
385
- });
386
- ```
387
-
388
- The dashboard will show a "Queued" badge with the reason: "Waiting for deploy-main (1 ahead)".
389
-
390
- ## Max concurrent runs
391
-
392
- The `max` field controls how many runs can execute simultaneously in the same group:
393
-
394
- ```typescript
395
- // Allow up to 3 parallel test runs per branch
396
- workflow('test', {
397
- concurrency: {
398
- group: (ctx) => `test-${ctx.branch}`,
399
- cancelInProgress: false,
400
- max: 3,
401
- },
402
- jobs: [/* ... */],
403
- });
404
- ```
405
-
406
- When `max: 1` (default), runs are fully serialized within the group.
407
-
408
- ## Group key examples
409
-
410
- ### Deploy per environment
411
-
412
- ```typescript
413
- workflow('deploy', {
414
- concurrency: {
415
- group: (ctx) => `deploy-${ctx.branch}`,
416
- cancelInProgress: true,
417
- },
418
- jobs: [
419
- job('deploy-staging', {
420
- runsOn: 'linux',
421
- context: 'staging',
422
- steps: [/* ... */],
423
- }),
424
- ],
425
- });
426
- ```
427
-
428
- ### Global singleton
429
-
430
- ```typescript
431
- // Only one migration can run at a time, regardless of branch
432
- workflow('migrate', {
433
- concurrency: {
434
- group: () => 'db-migration',
435
- cancelInProgress: false,
436
- },
437
- jobs: [/* ... */],
438
- });
439
- ```
440
-
441
- ### Environment-aware groups
442
-
443
- ```typescript
444
- // Serialize deploys per environment
445
- workflow('deploy', {
446
- concurrency: {
447
- group: (ctx) => {
448
- const env = ctx.branch === 'main' ? 'production' : 'staging';
449
- return `deploy-${env}`;
450
- },
451
- cancelInProgress: true,
452
- },
453
- jobs: [/* ... */],
454
- });
455
- ```
456
-
457
- ## Interaction with context protection
458
-
459
- When a workflow has both `concurrency` and `context` protection rules:
460
-
461
- 1. Context protection gates (required reviewers, wait timer) apply first
462
- 2. Concurrency group check happens after protection gates pass
463
- 3. If the run is queued by concurrency, it keeps its protection approval
464
-
465
- This means a run that passed approval won't need re-approval if it gets queued by concurrency.
466
-
467
- ## Cancelling queued runs
468
-
469
- Queued runs can be cancelled before they start executing. The cancel request removes them from the queue immediately -- they don't go through the grace period since no step is running.
470
-
471
- ## Job-level concurrency groups
472
-
473
- In addition to workflow-level concurrency, individual jobs can define their own concurrency group via the `concurrencyGroup` property. This controls concurrent execution at the job level rather than the workflow level. See [Contexts — concurrency groups](https://docs.kici.dev/user/contexts/#concurrency-groups) for details.
474
-
475
- ## Local execution
476
-
477
- `kici run <event> --local` is a real routed dispatch: your machine becomes an ephemeral agent behind the local dev plane, whose own orchestrator applies the same concurrency machinery described above. The `group` callback is evaluated agent-side against the simulated event, and `cancelInProgress` carries its usual semantics — `true` supersedes the older run in the group, `false` queues the newer one behind it.
478
-
479
- Coordination is scoped to that plane. The plane's state (including its database) lives under `~/.kici/local/`, so enforcement is per-machine and per-user: running the same workflow on two different machines does not serialize across them. For cross-host enforcement (queueing across agents, dashboard visibility), use `kici run remote` against a deployed orchestrator.
480
-
481
- See [`kici run <event> --local`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-run-event---local) for the rest of the local-run behavior, and [the local dev plane](https://docs.kici.dev/operator/orchestrator/local-dev-plane/) for the plane's state directory and lifecycle.
482
-
483
- ---
484
-
485
- _Source: `packages/sdk/src/types.ts` (WorkflowOptions.concurrency, JobOptions.concurrencyGroup)_
296
+ - [Organization-wide workflows](https://docs.kici.dev/user/global-workflows/) — why an approval gate is refused there.
486
297
 
487
298
  ---
488
299
 
@@ -509,8 +320,6 @@ Available rules:
509
320
 
510
321
  <!-- /help:contexts-protection -->
511
322
 
512
- Contexts represent deployment targets like `staging`, `production`, or `review/PR-*`. Each context can have its own variables, bound secrets, and protection rules that control when and how jobs targeting that context can execute.
513
-
514
323
  ## Overview
515
324
 
516
325
  A context in KiCI provides:
@@ -568,7 +377,7 @@ job('deploy-review', {
568
377
  });
569
378
  ```
570
379
 
571
- A pure function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is evaluated inline at dispatch with no init-job overhead. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
380
+ A dynamic context function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is resolved on the eval agent's init step before the job runs. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
572
381
 
573
382
  ### Multiple contexts per job
574
383
 
@@ -592,7 +401,7 @@ job('deploy', {
592
401
 
593
402
  **Merge order — last wins.** All bound contexts are resolved on every dispatch (webhook, scheduled, and test runs alike) and merged in array order. When the same secret or variable key is defined in more than one context, the later entry in the array wins. With `contexts: ['staging', 'my-testing']`, a key defined in both resolves to `my-testing`'s value; keys defined in only one are preserved. The longest-scope-path-wins rule still applies _within_ each context.
594
403
 
595
- **Protection rules combine all-must-pass.** A job must satisfy **every** bound context's gates — adding a context can never loosen access. Branch restrictions, trigger-type filters, and repo patterns must pass for all contexts; the minimum trust tier is the most restrictive across them; required reviewers are the union of all contexts' reviewers; the wait timer is the longest; and the hold expiry is the shortest. If a run is gated out, the rejection names which context and which rule rejected it (visible via `kici runs show <run-id>` and the run's rejection reason), so a mutually-exclusive set of rules surfaces as a clear failure rather than a silent perpetual rejection.
404
+ **Protection rules combine all-must-pass.** A job must satisfy **every** bound context's gates — adding a context can never loosen access. Branch restrictions, trigger-type filters, and repo patterns must pass for all contexts. The minimum trust tier is the most restrictive across them, and required reviewers are the union of all contexts' reviewers. The wait timer is the longest, and the hold expiry is the shortest. If a rule rejects a job, that job is not dispatched. It still appears on the run, as a failed job whose reason names the context and the rule that rejected it. Read the reason with `kici runs show <run-id>`, or on the run detail page.
596
405
 
597
406
  **Skip-on-test (allow-and-warn).** On a test or local run (`kici run remote`, `kici run <event> --local`), a bound context never rejects the run. Any bound context that disallows local execution (`allowLocalExecution: false`) — or that is not configured — is **skipped**: its variables and secrets are omitted from the merge and its gates are not evaluated. The run proceeds, and a user-visible warning naming the skipped context(s) is shown both on the `kici run remote` CLI output and on the dashboard run view. This makes the test-only-variables pattern work: with `contexts: ['staging', 'my-testing']` where only `my-testing` allows local execution, a test run resolves just `my-testing`'s variables and warns that `staging` was skipped. If every bound context is skipped, the job runs with no environment variables. This is intentionally different from a fixture `secrets:` mapping, which is fail-closed — see the [testing guide](https://docs.kici.dev/user/testing-guide/).
598
407
 
@@ -690,6 +499,34 @@ Allowed branches: main, release/*
690
499
 
691
500
  Jobs from other branches are rejected immediately with an error message.
692
501
 
502
+ **Most internally-triggered runs carry a branch.** KiCI starts these runs for itself: a [schedule](https://docs.kici.dev/user/sdk/triggers/) fire, a [custom event](https://docs.kici.dev/user/events/), a completion trigger, a failure batch, or an [invoke gate](https://docs.kici.dev/user/global-workflows/) summon. None comes from a branch push. A run with one source branch presents it:
503
+
504
+ - A **schedule** fire executes the default branch's workflow, so it presents the repository's default branch.
505
+ - A **`kiciEvent()` subscriber**, a **completion trigger** (`workflowComplete`, `jobComplete`) and an **invoke-gate summon** present the branch of the run behind them. That is the same branch the run behind them presented. For a pull-request run it is the branch the PR targets, not the contributor's own branch — so a subscriber of an event emitted by a PR against `main` presents `main`.
506
+
507
+ A branch restriction compares that branch against your patterns like any other run. So a nightly deploy bound to a `production` context restricted to `main` runs.
508
+
509
+ A branch restriction is not a trust control. It checks the branch a run presents, never where the code came from. A pull request against `main` presents `main`, whoever opened it. To gate on that, set a [minimum trust tier](https://docs.kici.dev/user/contexts/#minimum-trust) on the same context.
510
+
511
+ A run with no single source branch presents no branch, and a branch restriction then rejects it:
512
+
513
+ - A **failure batch** (`workflowsFailedBatch`). One accumulation window of failed runs causes it, on as many branches, so no one branch is its own.
514
+ - A **scaler event** (`kici.scaler.scale-up`, `kici.scaler.scale-down`). The orchestrator mints these itself, with no run behind them. See [autoscaling workflows](https://docs.kici.dev/user/workflows/autoscaling-workflows/).
515
+ - A **schedule** fire in a repository that has not pushed to its default branch since you upgraded KiCI. The default branch is captured when a push re-registers the workflows, so the first such push after the upgrade fixes it.
516
+ - Any event whose emitting run is no longer on record.
517
+
518
+ The rejection reason says so:
519
+
520
+ ```
521
+ Context 'production' restricts branches: this internally-triggered run carries no
522
+ branch, so no branch restriction can be satisfied - a scheduled run gains its
523
+ branch after the next push to the default branch re-registers the workflow;
524
+ alternatively bind a context without a branch restriction, or restrict by trigger
525
+ type instead
526
+ ```
527
+
528
+ To limit a context by how the run started instead of by branch, use a **trigger-type filter**: the trigger type is a real name (`schedule`, `kici_event`, `workflow_complete`, `job_complete`), so a filter that allows it works on these runs.
529
+
693
530
  ### Required reviewers
694
531
 
695
532
  Require manual approval before a job can proceed:
@@ -714,41 +551,44 @@ The job waits for the specified duration before proceeding. Useful for staged ro
714
551
 
715
552
  ### Minimum trust
716
553
 
717
- Gate job execution based on the contributor's trust tier for PR-triggered runs:
554
+ Hold a job whose run came from a fork:
718
555
 
719
556
  ```
720
- Minimum trust: known
557
+ Minimum trust: trusted
721
558
  ```
722
559
 
723
- | Value | Effect |
724
- | --------- | --------------------------------------------------- |
725
- | `known` | Blocks unknown contributors; allows known + trusted |
726
- | `trusted` | Blocks unknown + known; allows only trusted |
560
+ | Value | Effect |
561
+ | --------- | ---------------------------------------------------------- |
562
+ | `trusted` | Holds a run whose ref came from a fork |
563
+ | `known` | Same effect; the value is deprecated and removed at v1.0.0 |
564
+ | (unset) | No trust-based gating |
565
+
566
+ Both values block the same thing. Trust comes from the git ref, and that judgement has two answers: a ref in your repository is `trusted`, a ref from a fork is `unknown`. The value you declare still decides the wording of the hold reason.
567
+
568
+ When the gate holds a job, it enters the security approval queue. Someone with `ci_trust:write` or higher must approve it before execution proceeds.
727
569
 
728
- When a contributor does not meet the minimum trust level, the job is held in the security approval queue. Someone with `ci_trust:write` or higher must approve it before execution proceeds.
570
+ A run that resolved **no** tier passes the gate. A pull request from a source with no fork model resolves none, and so does an internal run whose inheritance lookup failed. See [trust tiers on internal triggers](https://docs.kici.dev/user/events/#trust-tiers-on-internal-triggers) for the full table.
729
571
 
730
- Trust tier is determined by the contributor's identity link and CI trust RBAC level:
572
+ The trust tier also affects which lock file a pull-request run uses: a trusted ref evaluates the head lock file, a fork ref evaluates the base branch's. A fork run additionally carries no install or registry secrets, and its build-cache writes are confined to that run. So a fork pull request cannot change what CI does, and cannot read a private-registry token, whether or not you set this gate.
731
573
 
732
- - **Trusted** -- identity-linked org member with `ci_trust:write+` AND provider write access
733
- - **Known** -- identity-linked member or verified collaborator via provider API
734
- - **Unknown** -- no identity link and no provider access, fork PRs
574
+ Set the gate on any context that carries a credential a fork run must not reach. See the [deployment checklist](https://docs.kici.dev/operator/security/security/#deployment-checklist-which-contexts-need-it).
735
575
 
736
- The trust tier also affects which lock file is used for PR-triggered runs: trusted contributors use the PR head lock file, while known and unknown contributors use the base branch lock file. This prevents untrusted workflow modifications from affecting execution.
576
+ An internally-triggered run carries a tier too. A schedule fire and the orchestrator's own lifecycle events are trusted. A `kiciEvent()` subscriber inherits the tier of the run that emitted the event, so a `minimumTrust` gate on the subscriber's context reads the **emitting** run's tier. See [trust tiers on internal triggers](https://docs.kici.dev/user/events/#trust-tiers-on-internal-triggers).
737
577
 
738
578
  See the [CI security architecture docs](https://docs.kici.dev/architecture/security/ci-security/) for the full trust resolution flow.
739
579
 
740
580
  ### Security approval queue
741
581
 
742
- When a PR is held for security review the org trust policy held it (a non-trusted contributor modified `.kici/` files, the PR came from a fork, or the contributor could not be resolved to a known identity), or a `minimumTrust` gate blocked the contributor — it enters the security approval queue. This is separate from the context approval queue: a security hold asks "is it safe to run this contributor's code at all?", while a context approval hold asks "should this job be promoted?". The two never cross — releasing a security hold needs `ci_trust:write` or higher, releasing a context approval hold needs `contexts:write` plus eligibility for one of the gate's clauses. See [Approval holds vs security holds](https://docs.kici.dev/architecture/approvals/#approval-holds-vs-security-holds) for the full comparison.
582
+ A pull request held for security review enters the security approval queue. Two things put it there: the organization's fork switch set to `hold`, and a `minimumTrust` gate blocking a fork run. This is separate from the context approval queue: a security hold asks "is it safe to run this contributor's code at all?", while a context approval hold asks "should this job be promoted?". The two never cross — releasing a security hold needs `ci_trust:write` or higher, releasing a context approval hold needs `contexts:write` plus eligibility for one of the gate's clauses. See [Approval holds vs security holds](https://docs.kici.dev/architecture/approvals/#approval-holds-vs-security-holds) for the full comparison.
743
583
 
744
584
  Held runs can be approved:
745
585
 
746
- - Via the **dashboard** in Settings > CI trust > Approval queue
586
+ - Via the **dashboard** on the [Approval queue](https://docs.kici.dev/user/dashboard/contexts-and-secrets/#approval-queue) page, which lists security and context holds together
747
587
  - Via a PR comment: `/kici approve` (commenter must have `ci_trust:write+`)
748
588
 
749
- A hold raised by the org trust policy covers the whole PR and uses the org's approval expiry (default 72 hours). A `minimumTrust` hold is raised by a context rather than by the org policy, so it uses that context's own hold expiry (default one hour).
589
+ A hold raised by the fork switch covers the whole pull request and uses the org's approval expiry (default 72 hours). A `minimumTrust` hold is raised by a context, so it uses that context's own hold expiry (default one hour). A job carrying both a reviewer approval hold and a security hold carries both expiries, and whichever comes first cancels the run.
750
590
 
751
- While the org trust policy is holding a pull request, your organization's global workflows do not run for it. Approving the hold releases that pull request's own workflows; it does not retroactively run the organization's global workflows for the event.
591
+ While the fork switch is holding a pull request, your organization's global workflows do not run for it. Approving the hold releases that pull request's own workflows; it does not retroactively run the organization's global workflows for the event.
752
592
 
753
593
  ### Concurrency limits
754
594
 
@@ -764,6 +604,10 @@ The concurrency limit is a positive integer; leave it unset for unlimited concur
764
604
  - **queue** -- new jobs wait in a FIFO queue (with configurable timeout, default 1 hour)
765
605
  - **cancel-pending** -- pending (queued) jobs are cancelled when the limit is reached
766
606
 
607
+ The children of a matrix job count individually against the limit. A three-child
608
+ matrix bound to a context with a limit of two dispatches two children and applies
609
+ the strategy above to the third.
610
+
767
611
  ## Dashboard management
768
612
 
769
613
  ### Creating contexts
@@ -791,7 +635,7 @@ Each context has four tabs:
791
635
 
792
636
  A job's bound deployment contexts are shown as chips on the run detail page (in the job metadata panel) in the order the job declared them, and the distinct set across a run's jobs appears as compact chips on the run list. For a multi-context job the chips read left-to-right in merge order — later contexts override earlier ones on key collisions. A `(dynamic)` chip marks a context whose name is computed at runtime; it resolves to the real name once the run starts. A job that binds a single context shows one chip; a job that binds none shows no chip.
793
637
 
794
- If a multi-context binding is gated out, the run's failure banner names which context and which rule rejected it (the same all-must-pass detail surfaced by `kici runs show <run-id>`).
638
+ A job a bound context rejects keeps its chips, and shows as failed because it never ran. Its failure reason names the context and the rule that rejected it. `kici runs show <run-id>` prints the same reason.
795
639
 
796
640
  ### Secrets management
797
641
 
@@ -844,857 +688,6 @@ This guide is split across the following pages:
844
688
 
845
689
  ---
846
690
 
847
- ## Dynamic values
848
-
849
- Source: https://docs.kici.dev/user/dynamic-values/
850
-
851
- Dynamic values let you compute `context`, `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.
852
-
853
- ```typescript
854
- job('deploy', {
855
- runsOn: ['default'],
856
- context: (event) => event.targetBranch,
857
- env: (event) => ({ BRANCH: event.targetBranch }),
858
- concurrencyGroup: (event) => `deploy-${event.targetBranch}`,
859
- steps: [/* ... */],
860
- });
861
- ```
862
-
863
- ```typescript
864
- job('deploy', {
865
- runsOn: 'default',
866
- // One shape everywhere: branch on the normalized event type.
867
- context: (event) => (event.type === 'pull_request' ? 'preview' : 'production'),
868
- steps: [/* ... */],
869
- });
870
- ```
871
-
872
- ## How it works
873
-
874
- When you define a dynamic value as a function, the compiler analyzes it at compile time to determine whether it is **pure** (can be evaluated without cloning the repo or running an init job).
875
-
876
- ### Pure functions (inline evaluation)
877
-
878
- A pure function is one that:
879
-
880
- - Is synchronous (no `async`/`await`)
881
- - Only references its parameters and local variables
882
- - Does not import or require external modules
883
- - Does not access globals like `process`, `fetch`, `console`, `setTimeout`, etc.
884
- - Uses only safe built-in constructors: `String`, `Number`, `Boolean`, `Array`, `Object`, `JSON`, `Math`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `encodeURIComponent`, `decodeURIComponent`, `encodeURI`, `decodeURI`
885
- - Does not use `this`, `new`, `class`, `throw`, `try`/`catch`, `delete`, `var`, `yield`, or mutation operators (`++`, `--`, `+=`, etc.)
886
-
887
- When the compiler detects a pure function, it serializes the function source directly into the lock file as an inline expression. At dispatch time, the orchestrator evaluates the expression in a sandboxed VM context (~0ms overhead) instead of dispatching an init job.
888
-
889
- **Examples of pure functions:**
890
-
891
- ```typescript
892
- // Simple branch extraction
893
- context: (event) => event.targetBranch;
894
-
895
- // Object literal with string operations
896
- env: (event) => ({ BRANCH: event.targetBranch });
897
-
898
- // Concatenation with event data
899
- concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
900
-
901
- // Using safe globals
902
- env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
903
-
904
- // Local variables are fine
905
- context: (event) => {
906
- const parts = event.targetBranch.split('/');
907
- return parts[parts.length - 1];
908
- };
909
- ```
910
-
911
- ### Impure functions (init-job evaluation)
912
-
913
- If the compiler determines a function is impure, it prints a `warning [W101]` naming the affected field (`context`, `env`, or `concurrencyGroup`), the reason the function was judged impure, and the ~5-10 second init-job cost — and compilation still succeeds. The function falls back to the two-phase init model. This means:
914
-
915
- 1. The orchestrator dispatches a special `__init__` job to a builder agent
916
- 2. The builder agent clones the repository and evaluates the function
917
- 3. The resolved values are sent back to the orchestrator
918
- 4. The orchestrator dispatches the real execution job with the resolved values
919
-
920
- This adds approximately 5-10 seconds of overhead for cloning and evaluation.
921
-
922
- `kici preview` lists the injected `__init__` job under each affected job, so you can spot the init-job cost before the first run.
923
-
924
- **Examples of impure functions (will use init job):**
925
-
926
- ```typescript
927
- // Async functions cannot be inlined
928
- context: async (event) => await lookupEnv(event.targetBranch);
929
-
930
- // External module references
931
- env: (event) => {
932
- const config = require('./config');
933
- return config.env;
934
- };
935
-
936
- // Process/global access
937
- context: (event) => process.env.DEFAULT_ENV || 'staging';
938
-
939
- // Dynamic imports
940
- env: async (event) => {
941
- const m = await import('./config.js');
942
- return m.default;
943
- };
944
- ```
945
-
946
- ## Performance comparison
947
-
948
- | Evaluation path | Overhead | When used |
949
- | ------------------------------------ | -------- | --------------------------------------------------------------- |
950
- | Static value (string/object literal) | ~0ms | `context: 'staging'` |
951
- | Inline expression (pure function) | ~0ms | `context: (event) => event.targetBranch` |
952
- | Init job (impure function) | ~5-10s | `context: async (event) => await lookupEnv(event.targetBranch)` |
953
-
954
- ## Tips
955
-
956
- - **Write pure functions whenever possible** to avoid the init-job delay. Most context and env computations only need the event payload data.
957
- - **Check compiler warnings** -- the compiler prints a `warning [W101]` when a function is classified as impure, naming the reason and the ~5-10s init-job cost. Run `kici preview` to see the injected `__init__` job listed under each affected job before your first run.
958
- - **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.
959
- - **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where pure vs. impure functions run relative to rules, hooks, and step bodies.
960
- - **The event parameter is the normalized event envelope** — the same shape rules receive as `ctx.event`: `{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }` (see the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/) for the complete schema). Narrow on `event.type` (`'push'`, `'pull_request'`, `'tag'`, …) to branch per trigger kind. The raw provider webhook body is nested at `event.payload` (for GitHub pushes: `payload.ref`, `payload.after`, `payload.repository`, …).
961
-
962
- ---
963
-
964
- ## Environment variables
965
-
966
- Source: https://docs.kici.dev/user/env-vars/
967
-
968
- 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 another KiCI environment (e.g. a testing instance) or a custom OIDC provider.
969
-
970
- ## Authentication
971
-
972
- | Variable | Description | Default |
973
- | --------------------- | -------------------------------------- | -------------------------------------------- |
974
- | `KICI_OIDC_ISSUER` | OIDC issuer URL for authentication | `https://auth.kici.dev/realms/kici-internal` |
975
- | `KICI_OIDC_CLIENT_ID` | OIDC client ID for the CLI application | `kici-cli` |
976
- | `KICI_PLATFORM_URL` | Platform API base URL | `https://api.kici.dev` |
977
- | `KICI_CONFIG_DIR` | Override the KiCI config directory | `~/.kici` |
978
-
979
- ## Browser behavior
980
-
981
- | Variable | Description | Default |
982
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
983
- | `KICI_BROWSER_CMD` | Custom browser command for OAuth login. Supports `{url}` placeholder. Set to `none` to suppress browser opening and print the URL to stdout instead. | Uses the system default browser |
984
- | `KICI_CALLBACK_PORT` | Fixed port for the OAuth PKCE callback server. Useful when firewall rules require a known port. | Random available port |
985
-
986
- ## Development
987
-
988
- | Variable | Description | Default |
989
- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
990
- | `KICI_DEV` | Enable development mode. When `true`, uses prerelease-compatible version ranges (`>=0.0.1-0`) for dev dependencies and skips npm version resolution. | unset |
991
- | `KICI_DEBUG` | Enable debug logging. When `true`, prints verbose diagnostics (SDK alias resolution, step-level debug logs, stack traces on errors). Equivalent to the `--debug` CLI flag. | unset |
992
-
993
- ## Local dev plane
994
-
995
- Read by the [local dev plane](https://docs.kici.dev/user/cli/authoring-and-local/#kici-local) that `kici run <event> --local` dispatches through.
996
-
997
- | Variable | Description | Default |
998
- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
999
- | `KICI_LOCAL_ORCH_PORT` | Port the plane orchestrator listens on (HTTP + WebSocket). Change it when another process already holds the default. | `4319` |
1000
- | `KICI_LOCAL_PG_PORT` | Port the plane's PostgreSQL listens on. | `45432` |
1001
- | `KICI_LOCAL_PG_MODE` | Set to `podman` to force the container PostgreSQL fallback instead of the embedded binary. | Embedded PostgreSQL |
1002
- | `KICI_LOCAL_ACCEPTANCE_TIMEOUT_MS` | How long a local run waits for an agent to claim its first job before failing fast. Raise it on a slow host; the run still fails quickly when no scaler label set matches `runsOn`. | `120000` (2 minutes) |
1003
-
1004
- ## CI detection
1005
-
1006
- The CLI also reads the conventional CI markers your CI provider sets. They are not KiCI variables — KiCI only consumes them.
1007
-
1008
- | Variable | Description | Default |
1009
- | ---------------- | -------------------------------------------- | ------- |
1010
- | `CI` | Generic CI marker. Set by most CI providers. | unset |
1011
- | `GITHUB_ACTIONS` | Set to `true` by GitHub Actions. | unset |
1012
- | `GITLAB_CI` | Set to `true` by GitLab CI/CD. | unset |
1013
-
1014
- ### How `CI` is interpreted
1015
-
1016
- `kici` treats the environment as CI when `CI`, `GITHUB_ACTIONS`, or `GITLAB_CI` is set to any value other than an explicit opt-out. `0` and `false` are the opt-outs, compared case-insensitively, so `CI=0`, `CI=false`, and `CI=False` all mean "not CI". Surrounding whitespace is ignored, and a value that is empty or only whitespace (`CI=`) is treated as unset.
1017
-
1018
- A vendor marker outranks the generic opt-out: `CI=false GITHUB_ACTIONS=true` is still CI, because an explicit vendor marker names a real runner rather than a preference.
1019
-
1020
- This affects which login flow `kici login` chooses (browser vs device) and whether interactive commands such as `kici init` prompt.
1021
-
1022
- ## Usage examples
1023
-
1024
- ### CI/CD environment
1025
-
1026
- Authenticate with a pre-existing API key (no browser needed):
1027
-
1028
- ```bash
1029
- kici login --token "$KICI_API_KEY"
1030
- ```
1031
-
1032
- ### Targeting another environment or custom OIDC provider
1033
-
1034
- `kici login` targets the hosted KiCI Platform by default. To point the CLI at another KiCI environment (e.g. a testing instance) or a custom OIDC provider, override the defaults:
1035
-
1036
- ```bash
1037
- export KICI_OIDC_ISSUER=https://your-idp.example.com
1038
- export KICI_OIDC_CLIENT_ID=your-client-id
1039
- export KICI_PLATFORM_URL=https://your-platform.example.com
1040
- kici login
1041
- ```
1042
-
1043
- ### Headless SSH session
1044
-
1045
- The CLI auto-detects headless environments and uses the device flow. To force PKCE with URL output instead:
1046
-
1047
- ```bash
1048
- export KICI_BROWSER_CMD=none
1049
- kici login
1050
- ```
1051
-
1052
- This prints the authorization URL to stdout as `KICI_AUTH_URL=<url>`. Open the URL in any browser to complete authentication.
1053
-
1054
- ### Fixed callback port
1055
-
1056
- When behind a firewall or using port forwarding:
1057
-
1058
- ```bash
1059
- export KICI_CALLBACK_PORT=19876
1060
- kici login
1061
- ```
1062
-
1063
- ### Custom config location
1064
-
1065
- Store the KiCI config in a non-default location:
1066
-
1067
- ```bash
1068
- export KICI_CONFIG_DIR=/tmp/kici-test
1069
- kici login
1070
- ```
1071
-
1072
- ---
1073
-
1074
- ## Event system
1075
-
1076
- Source: https://docs.kici.dev/user/events/
1077
-
1078
- 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.
1079
-
1080
- ## Overview
1081
-
1082
- Git-based triggers (`push()`, `pr()`, `tag()`, `comment()`, `review()`, `release()`, etc.) work immediately after you commit your lock file. When a GitHub webhook arrives, the orchestrator fetches your lock file and evaluates triggers on the spot -- no advance setup needed.
1083
-
1084
- Event-based triggers work differently. The orchestrator needs to know about them _before_ the event arrives. This is because event-based triggers are matched against a pre-built registration index rather than being evaluated per-event from a lock file fetch. The six event-based trigger types are:
1085
-
1086
- - `kiciEvent()` -- custom events emitted from workflow steps
1087
- - `workflowComplete()` -- fires when a workflow finishes
1088
- - `jobComplete()` -- fires when a specific job finishes
1089
- - `genericWebhook()` -- HTTP webhooks from external services
1090
- - `schedule()` -- cron-based time triggers
1091
- - `lifecycle()` -- orchestrator lifecycle events (workflow completion, job failure, registration updates)
1092
-
1093
- All six require the **registration model** to function -- covered in detail below.
1094
-
1095
- ## Event types
1096
-
1097
- ### Custom events
1098
-
1099
- Custom events are user-defined events emitted from workflow steps using `ctx.emit()`. Use `kiciEvent()` to listen for them.
1100
-
1101
- ```typescript
1102
- import { kiciEvent } from '@kici-dev/sdk';
1103
-
1104
- // Listen for a custom event by name
1105
- kiciEvent({ name: 'deploy-complete' });
1106
-
1107
- // With payload matching (JSONPath)
1108
- kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } });
1109
-
1110
- // With negative filter
1111
- kiciEvent({ name: 'deploy-complete', not: { '$.env': 'staging' } });
1112
-
1113
- // From a specific repository
1114
- kiciEvent({ name: 'deploy-complete', source: 'org/infra-repo' });
1115
- ```
1116
-
1117
- **Config options:** `name` (required), `match`, `not`, `source`, `description`.
1118
-
1119
- ### System events
1120
-
1121
- The orchestrator automatically emits completion events when workflows and jobs finish. No manual emission needed -- these fire automatically.
1122
-
1123
- **Workflow completion:**
1124
-
1125
- ```typescript
1126
- import { workflowComplete } from '@kici-dev/sdk';
1127
-
1128
- // Any workflow completion
1129
- workflowComplete();
1130
-
1131
- // Specific workflow by name
1132
- workflowComplete({ name: 'build' });
1133
-
1134
- // Only successful completions
1135
- workflowComplete({ name: 'build', status: ['success'] });
1136
- ```
1137
-
1138
- **Config options:** `name`, `status` (`'success'`, `'failed'`, `'cancelled'`), `source`, `description`.
1139
-
1140
- **Job completion:**
1141
-
1142
- ```typescript
1143
- import { jobComplete } from '@kici-dev/sdk';
1144
-
1145
- // Any job completion
1146
- jobComplete();
1147
-
1148
- // Specific workflow + job
1149
- jobComplete({ workflow: 'build', job: 'test' });
1150
-
1151
- // Only failures
1152
- jobComplete({ workflow: 'build', job: 'test', status: ['failed'] });
1153
- ```
1154
-
1155
- **Config options:** `workflow`, `job`, `status` (`'success'`, `'failed'`, `'cancelled'`, `'skipped'`), `source`, `description`.
1156
-
1157
- ### External events
1158
-
1159
- Generic webhooks let you trigger workflows from any HTTP service -- Stripe, ArgoCD, Slack, Grafana, or your own internal services.
1160
-
1161
- ```typescript
1162
- import { genericWebhook } from '@kici-dev/sdk';
1163
-
1164
- // Match any event from a source
1165
- genericWebhook({ source: 'stripe' });
1166
-
1167
- // Match specific event types
1168
- genericWebhook({ source: 'stripe', events: ['invoice.paid'] });
1169
-
1170
- // With HMAC-SHA256 signature verification
1171
- genericWebhook({
1172
- source: 'stripe',
1173
- events: ['invoice.paid'],
1174
- auth: {
1175
- method: 'hmac-sha256',
1176
- secret: 'stripe-signing-key',
1177
- signatureHeader: 'stripe-signature',
1178
- },
1179
- });
1180
-
1181
- // With API key auth
1182
- genericWebhook({
1183
- source: 'slack',
1184
- auth: { method: 'api-key', secret: 'slack-token' },
1185
- });
1186
- ```
1187
-
1188
- **Config options:** `source` (required), `events`, `match`, `not`, `auth`, `path`, `description`.
1189
-
1190
- The `source` field MUST match the `--name` that an operator passed to `kici-admin source add generic --name <name>` when the source was created — that string is the source's identifier in the orchestrator. Generic webhook sources must be created by an operator before events can be received; see [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) for setup instructions.
1191
-
1192
- ### Schedule events
1193
-
1194
- Cron-based triggers evaluated by the orchestrator on a periodic interval. Only the Raft leader evaluates schedules in a clustered deployment.
1195
-
1196
- ```typescript
1197
- import { schedule } from '@kici-dev/sdk';
1198
-
1199
- // Run every hour
1200
- schedule({ cron: '0 * * * *' });
1201
-
1202
- // Run daily at 2 AM UTC
1203
- schedule({ cron: '0 2 * * *' });
1204
-
1205
- // Run weekly on Mondays at 9 AM Eastern
1206
- schedule({ cron: '0 9 * * 1', timezone: 'America/New_York' });
1207
- ```
1208
-
1209
- **Config options:** `cron` (required), `timezone` (defaults to `'UTC'`), `description`.
1210
-
1211
- ### Lifecycle events
1212
-
1213
- Lifecycle triggers listen for orchestrator-level events related to workflow execution and system state changes.
1214
-
1215
- ```typescript
1216
- import { lifecycle } from '@kici-dev/sdk';
1217
-
1218
- // Trigger when any workflow completes
1219
- lifecycle({ events: ['workflow_complete'] });
1220
-
1221
- // Trigger on job failures from a specific repo
1222
- lifecycle({ events: ['job_failed'], sources: ['org/deploy-repo'] });
1223
-
1224
- // Trigger when registrations are updated
1225
- lifecycle({ events: ['registration_updated'] });
1226
- ```
1227
-
1228
- **Available events:** `'workflow_complete'`, `'job_complete'`, `'job_failed'`, `'registration_updated'`.
1229
-
1230
- **Config options:** `events` (required), `sources`, `description`.
1231
-
1232
- ## The registration model
1233
-
1234
- This is the most important concept for understanding event-based triggers.
1235
-
1236
- ### Why registrations exist
1237
-
1238
- When a GitHub webhook arrives (push, PR, etc.), the orchestrator fetches your lock file from the repository and evaluates triggers on the spot. This works because the event itself tells the orchestrator _which repository_ to look at.
1239
-
1240
- Event-based triggers are different. When a cron timer fires or a custom event is emitted, there is no incoming webhook pointing to a specific repository. The orchestrator needs to know _in advance_ which workflows care about which events. That is what the registration model provides: a pre-built index of event-based workflows.
1241
-
1242
- ### How registration works
1243
-
1244
- 1. You define a workflow with an event-based trigger (e.g., `schedule()`, `kiciEvent()`, `genericWebhook()`)
1245
- 2. You compile the workflow (`kici compile`), which produces a lock file
1246
- 3. You push the lock file to your repository's **default branch** (e.g., `main` or `master`)
1247
- 4. The orchestrator receives the push webhook, detects it targets the default branch, and extracts all workflows with event-based triggers from the lock file
1248
- 5. Those workflows are stored in the orchestrator's registration database
1249
- 6. From that point on, matching events will trigger those workflows
1250
-
1251
- ### Key implications
1252
-
1253
- - **Event-based workflows do not trigger until you push to the default branch.** If you add a new `schedule()` workflow, it will not start running until you merge to your default branch. This is by design -- the orchestrator cannot match events to workflows it does not know about.
1254
-
1255
- - **Registration is automatic.** There is no manual setup. Push your code, and the orchestrator handles the rest.
1256
-
1257
- - **Registrations refresh on every default-branch push.** If you add, remove, or modify event-based workflows and push to the default branch, the orchestrator updates its registration index automatically. Removed workflows stop triggering. New workflows start triggering.
1258
-
1259
- - **Git-based triggers are unaffected.** Triggers like `push()`, `pr()`, and `tag()` do not use registrations. They work immediately from any branch because the orchestrator evaluates them per-event from the lock file.
1260
-
1261
- ### Practical example
1262
-
1263
- You create a nightly build workflow:
1264
-
1265
- ```typescript
1266
- import { workflow, job, step, schedule } from '@kici-dev/sdk';
1267
-
1268
- export default workflow('nightly-build', {
1269
- on: schedule({ cron: '0 2 * * *' }),
1270
- jobs: [
1271
- job('build', {
1272
- runsOn: 'linux',
1273
- steps: [
1274
- step('build', async ({ $ }) => {
1275
- await $`pnpm build`;
1276
- }),
1277
- ],
1278
- }),
1279
- ],
1280
- });
1281
- ```
1282
-
1283
- You compile it, commit the lock file, and push to a feature branch. **Nothing happens** -- the cron will not fire because the orchestrator has not registered this workflow yet.
1284
-
1285
- You merge the feature branch into `main`. On the merge push, the orchestrator extracts the `nightly-build` workflow (it has a `ScheduleTrigger`) and registers it. Starting at the next 2 AM UTC, the workflow will trigger.
1286
-
1287
- ## How events are matched
1288
-
1289
- When an event arrives, the orchestrator follows this flow:
1290
-
1291
- 1. **Event received** -- a custom event is emitted by a step, a cron timer fires, or a generic webhook arrives
1292
- 2. **Registration lookup** -- the orchestrator queries its registration index for workflows matching the event type (e.g., all workflows with `ScheduleTrigger` for a cron fire, or all workflows with `KiciEventTrigger` for a custom event)
1293
- 3. **Trigger evaluation** -- for each candidate workflow, the orchestrator evaluates the trigger conditions: event name patterns, payload matching, status filters, source filters
1294
- 4. **Dispatch** -- matched workflows are dispatched to agents for execution, following the same job queue and agent routing as git-triggered workflows
1295
-
1296
- This lookup is fast because the registration index is held in memory and refreshed only when the registry version changes (on default-branch pushes).
1297
-
1298
- ### Cross-source webhook delivery
1299
-
1300
- The catch-all `webhook()` trigger (see [SDK reference: webhook()](https://docs.kici.dev/user/sdk/triggers/#webhook)) participates in this same registration lookup, but with one twist: it fires for matching events arriving via **any** inbound webhook source in the same org, not just the source the workflow's repo is bound to. The orchestrator maintains a `(customerId, eventName)` index over webhook trigger registrations and consults it on every inbound generic webhook.
1301
-
1302
- The lookup is structurally org-isolated — a generic webhook delivered to org A can never reach a workflow registered against org B, because foreign-org rows live in a different bucket of the index. When a webhook fires across sources, the runtime clone token, repo URL, and check-status posting all come from the **registration's** source bundle, not the inbound source. The inbound source contributes only the event payload.
1303
-
1304
- ## Circuit breaker
1305
-
1306
- Events can trigger workflows that emit more events, creating chains. The circuit breaker prevents runaway event storms.
1307
-
1308
- ### Chain depth limit
1309
-
1310
- Each event carries a `chainDepth` counter. When a workflow triggered by an event emits a new event, the new event's chain depth increments. The orchestrator rejects events that exceed the maximum chain depth.
1311
-
1312
- - **Default limit:** 10 levels deep
1313
- - **What happens when hit:** the event is dropped and logged. It is not queued for later delivery.
1314
-
1315
- For example: Workflow A emits event X (depth 0) -> Workflow B triggers, emits event Y (depth 1) -> ... -> at depth 10, any further emitted events are dropped.
1316
-
1317
- ### Rate limiting
1318
-
1319
- Each workflow is rate-limited on how many events it can process per minute, using a sliding window.
1320
-
1321
- - **Default limit:** 100 events per workflow per minute
1322
- - **What happens when hit:** additional events for that workflow are dropped and logged until the window clears.
1323
-
1324
- These defaults are hardcoded in the orchestrator and are not currently configurable via environment variables.
1325
-
1326
- ## Delivery guarantees
1327
-
1328
- KiCI's event router delivers every accepted event with **at-least-once** semantics:
1329
-
1330
- - An event that passes the circuit breaker (chain depth + rate limit) and commits
1331
- to the `kici_events` table is guaranteed to dispatch to all matching workflows
1332
- at least once.
1333
- - Each dispatch attempt acquires a short-lived lease (default 60 s) on the row.
1334
- If the dispatching node crashes or the handler throws, the lease expires (or
1335
- is released on failure) and the event is automatically retried.
1336
- - The retry policy is exponential backoff with full jitter: base 5 s, cap 5 min,
1337
- up to 5 attempts before the event lands in the **DLQ** (dead-letter queue).
1338
- Operators triage DLQ entries via `kici-admin event-dlq list / count / retry / discard`.
1339
-
1340
- **What this means for workflow authors:**
1341
-
1342
- - **Make event handlers idempotent.** A retried dispatch may run a handler more
1343
- than once (e.g. if the first attempt threw after a partial side-effect).
1344
- Workflows that mutate external state should use idempotency keys, conditional
1345
- writes, or other deduplication patterns — same advice as for any distributed
1346
- CI system.
1347
- - **Schedule fires are at-least-once too.** A cron schedule that fires while a
1348
- leader is being killed will commit (atomically with `cron_last_fired`) or roll
1349
- back together — never half. Recovery on the new leader does not backfill
1350
- multiple missed instants; if your workflow needs at-least-N guarantees across
1351
- outages, drive it from a different mechanism (e.g. a workflow that runs more
1352
- frequently and emits its own custom event).
1353
- - **Drops are still possible — and visible.** Events rejected by the circuit
1354
- breaker (chain depth or rate limit exceeded) are dropped and logged, not
1355
- retried. That's a deliberate safety mechanism; the metric to watch is
1356
- `kici_orch_events_dropped_total{reason}`.
1357
-
1358
- ## Emitting custom events
1359
-
1360
- Custom events are emitted from workflow steps using `ctx.emit()`. You can optionally define typed event schemas using `defineEvent()`.
1361
-
1362
- ### Basic emission
1363
-
1364
- ```typescript
1365
- import { workflow, job, step, push } from '@kici-dev/sdk';
1366
-
1367
- export default workflow('build', {
1368
- on: push({ branches: 'main' }),
1369
- jobs: [
1370
- job('build', {
1371
- runsOn: 'linux',
1372
- steps: [
1373
- step('build', async ({ $ }) => {
1374
- await $`pnpm build`;
1375
- }),
1376
- step('notify', async (ctx) => {
1377
- await ctx.emit('build-complete', {
1378
- version: '1.0.0',
1379
- success: true,
1380
- });
1381
- }),
1382
- ],
1383
- }),
1384
- ],
1385
- });
1386
- ```
1387
-
1388
- ### Typed event definitions
1389
-
1390
- Use `defineEvent()` with Zod schemas to create a typed contract for event payloads:
1391
-
1392
- ```typescript
1393
- import { defineEvent, z } from '@kici-dev/sdk';
1394
-
1395
- export const deployComplete = defineEvent(
1396
- 'deploy-complete',
1397
- z.object({
1398
- env: z.string(),
1399
- version: z.string(),
1400
- services: z.array(z.string()),
1401
- }),
1402
- );
1403
- ```
1404
-
1405
- Then emit using the definition — the payload is checked against the schema:
1406
-
1407
- ```typescript
1408
- step('emit', async (ctx) => {
1409
- await ctx.emit(deployComplete, {
1410
- env: 'prod',
1411
- version: '1.2.3',
1412
- services: ['api', 'web'],
1413
- });
1414
- });
1415
- ```
1416
-
1417
- And consume in another workflow:
1418
-
1419
- ```typescript
1420
- import { workflow, job, step, kiciEvent } from '@kici-dev/sdk';
1421
-
1422
- export default workflow('post-deploy', {
1423
- on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }),
1424
- jobs: [
1425
- job('smoke-test', {
1426
- runsOn: 'linux',
1427
- steps: [
1428
- step('test', async ({ $ }) => {
1429
- await $`./scripts/smoke-test.sh`;
1430
- }),
1431
- ],
1432
- }),
1433
- ],
1434
- });
1435
- ```
1436
-
1437
- Custom events are delivered immediately when emitted (mid-workflow, not queued until workflow completion). See the [SDK reference: emitting events](https://docs.kici.dev/user/sdk/validation-events/#emitting-events) section for the full `ctx.emit()` API.
1438
-
1439
- ## See also
1440
-
1441
- - [SDK reference: event triggers](https://docs.kici.dev/user/sdk/triggers/#event-triggers) -- complete API signatures for all trigger builders
1442
- - [SDK reference: emitting events](https://docs.kici.dev/user/sdk/validation-events/#emitting-events) -- `ctx.emit()` and `defineEvent()` API
1443
- - [Workflow patterns: workflow chaining](https://docs.kici.dev/user/patterns/integrations/#workflow-chaining) -- examples of event-driven workflow chains
1444
- - [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) -- configuring generic webhook sources, trust relationships, and event routing
1445
- - [Architecture: event system](https://docs.kici.dev/architecture/webhooks/event-system/) -- internal event routing design, registration model, cluster synchronization
1446
-
1447
- ---
1448
-
1449
- ## Global workflows
1450
-
1451
- Source: https://docs.kici.dev/user/global-workflows/
1452
-
1453
- 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."
1454
-
1455
- If you've only ever used per-repo workflows so far, start with the mental model section — global workflows add two new concepts (workflow repo vs. source repo, and authoring vs. source axes) that show up everywhere from SDK syntax to dashboard settings.
1456
-
1457
- ## Mental model
1458
-
1459
- | Term | Meaning |
1460
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1461
- | Workflow repo | The repo whose `.kici/workflows/*.ts` file **declares** the global workflow. Holds the steps. Also known as the _authoring_ repo. |
1462
- | Source repo | The repo that **emits** the event (push / PR / tag / ...) that causes the global workflow to fire. The agent checks out this repo as the working copy. |
1463
- | Global | A workflow whose trigger carries one or more `repos:` glob patterns. The presence of `repos:` is what classifies a workflow as global. |
1464
- | Authoring axis | Policy that answers "which repos may **author** global workflows?" Controlled by the allow-list in the dashboard's _Allowed author repos_ setting. |
1465
- | Source axis | Policy that answers "which **source** repos' events are allowed to trigger global workflows?" Controlled by the deny-list in _Blocked source repos_. |
1466
-
1467
- The two axes are independent. A global workflow fires only if it passes **both** — its authoring repo is allowed AND the source repo is not denied.
1468
-
1469
- ## Declaring a global workflow
1470
-
1471
- Add `repos:` to any trigger. Any workflow with at least one `repos:`-bearing trigger becomes global automatically; no separate flag is required.
1472
-
1473
- ```ts
1474
- import { workflow, job, step, push } from '@kici-dev/sdk';
1475
-
1476
- export default workflow('org-lint', {
1477
- on: [
1478
- push({
1479
- repos: ['myorg/*', '!myorg/archived-*'],
1480
- branches: ['main'],
1481
- }),
1482
- ],
1483
- jobs: [
1484
- job('lint', {
1485
- steps: [
1486
- step('lint-all', async ({ $, env }) => {
1487
- await $`echo source=${env.KICI_SOURCE_REPO_PATH ?? 'unknown'}`;
1488
- await $`npm run lint`;
1489
- }),
1490
- ],
1491
- }),
1492
- ],
1493
- });
1494
- ```
1495
-
1496
- Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org.
1497
-
1498
- ### At a dual-repo checkout
1499
-
1500
- The agent receives two sets of context during a global workflow execution:
1501
-
1502
- | `env` var | Points to |
1503
- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
1504
- | `KICI_SOURCE_REPO_PATH` | The **source** repo's working tree (the repo that emitted the event). This is the repo the job's `$` / `git` commands operate on by default. |
1505
- | `KICI_WORKFLOW_REPO_PATH` | The **workflow** repo's working tree (the repo that authored the workflow). Useful for reading shared scripts or config from your CI repo. |
1506
-
1507
- Source repo secrets are **not** available to a global workflow's job by default — see _Elevated access_ below.
1508
-
1509
- ## Enabling global workflows
1510
-
1511
- Global workflows are **opt-in per org**. In a fresh org, `repos:`-bearing workflows are registered but never dispatched.
1512
-
1513
- 1. Open the dashboard → **Settings → Global workflows**.
1514
- 2. Turn on **Enable global workflows** (the master toggle). This is the kill-switch — every other toggle below is ignored while this is off.
1515
- 3. Decide which authoring/source controls you need:
1516
-
1517
- | Setting | What it controls | Typical use |
1518
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
1519
- | Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
1520
- | Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
1521
- | Elevated access | Authoring repos listed here get **read access to source-repo secrets** during execution. Globs matched against the authoring repo identifier. | A `myorg/ci-deploy` repo that needs to read a source repo's `NPM_TOKEN` to publish releases. |
1522
-
1523
- All three lists accept globs. Leading `!` inside a single pattern is not supported here; negation is via the list-is-implicit-deny semantics, so keep it simple (`myorg/ci-*`, `myorg/platform-*`).
1524
-
1525
- ### Saving and reverting
1526
-
1527
- The page is a two-state editor — changes are local until you click **Save changes**, and you can abandon them with **Discard changes**. There is no partial save; the PATCH is all-or-nothing per save click.
1528
-
1529
- ## Security model
1530
-
1531
- ### Two independent axes
1532
-
1533
- A global workflow fires only if:
1534
-
1535
- 1. **The authoring repo is allowed.** If _Allowed author repos_ is ON, the workflow's authoring repo must match at least one allow-list glob. If OFF, any repo may author. Enforced at two points:
1536
- - At registration time (extraction from the lock file — non-matching globals are dropped with a warning).
1537
- - At dispatch time (defense-in-depth — policy changes after registration still take effect).
1538
- 2. **The source repo is not denied.** If the event's source repo matches any glob in _Blocked source repos_, the global workflow is skipped. Enforced at dispatch time.
1539
-
1540
- Both checks are logged to the orchestrator. Grep the logs for `Skipping global workflow` to see enforcement in action.
1541
-
1542
- ### Elevated access (source-repo secrets)
1543
-
1544
- By default a global workflow's job runs with credentials scoped to the **workflow** repo — it can clone both repos but cannot read the source repo's scoped secrets. That's the safe default: a random workflow in `myorg/ci-pipelines` does not get read access to secrets in `myorg/backend` just because it runs on a push there.
1545
-
1546
- Adding the authoring repo to the _Elevated access_ list flips that: the job receives the source repo's secret context, so deploy and release flows that need `NPM_TOKEN` / `AWS_ROLE_ARN` / etc. from the source repo can read them. Treat elevated repos as effective owners of every source repo's CI secrets — only add repos you fully trust.
1547
-
1548
- ## When does it fire?
1549
-
1550
- Same-repo globals (a workflow in `myorg/app` with `repos: ['myorg/app']`) fire on pushes to `myorg/app`. Cross-repo globals fire on pushes to any source repo whose identifier matches a glob on the authoring workflow's trigger. The orchestrator de-duplicates between the per-repo and cross-repo matching passes, so a single event produces at most one run per (workflow, source-repo, trigger) triple.
1551
-
1552
- Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workflowRun()`, etc. all accept `repos:`. `kiciEvent()` / `schedule()` / cron-like triggers have no source repo, so they're always per-org-registered regardless of `repos:`.
1553
-
1554
- ## Troubleshooting
1555
-
1556
- | Symptom | Likely cause | Where to look |
1557
- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
1558
- | Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` / `Skipping global workflow registration: not permitted` |
1559
- | `repos:` has no effect — workflow only fires on its own repo | Master toggle OFF. Without opt-in, the orchestrator treats the workflow as per-repo-only. | Dashboard → Settings → Global workflows (top toggle) |
1560
- | Source repo secrets unavailable in a global job | Expected default — elevate the authoring repo to grant access. | Dashboard → Settings → Global workflows → _Elevated access_ |
1561
- | Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
1562
-
1563
- ## See also
1564
-
1565
- - [Architecture — global workflows](https://docs.kici.dev/architecture/global-workflows/) — dual-query dispatch flow, cross-provider auth, security model, lock-file schema.
1566
- - [Universal-git provider](https://docs.kici.dev/user/providers/universal-git/#global-workflows) — how global workflows interact with `generic:<orgId>:<sourceId>` routing keys.
1567
- - [SDK reference](https://docs.kici.dev/user/sdk-reference/) — the full set of triggers that accept `repos:`.
1568
-
1569
- ---
1570
-
1571
- ## Idempotent steps and check mode
1572
-
1573
- Source: https://docs.kici.dev/user/idempotent-steps/
1574
-
1575
- An **idempotent step** describes _desired state_ rather than a fixed sequence of
1576
- commands. You give the step a `check` function that inspects the world and a
1577
- `run` function that converges it. KiCI then executes the workflow in one of two
1578
- modes:
1579
-
1580
- - **Apply mode** (the default): for each step, `check()` runs first; on drift the
1581
- step applies the change; when already in sync the step is skipped.
1582
- - **Check mode** (`--check`): for each step, `check()` runs and KiCI reports what
1583
- _would_ change — **without changing anything**. This is the same model as a
1584
- dry-run plan: you see the drift before any side effect happens.
1585
-
1586
- This turns a workflow into convergent configuration management: re-running an
1587
- apply is safe (in-sync steps do nothing), and a check-mode run is a read-only
1588
- preview you can gate a build on.
1589
-
1590
- ## Authoring a checked step
1591
-
1592
- Add a `check` facet to the existing `step()` factory. When `check` is present,
1593
- `run` becomes the _apply_ function and receives the drift value `check`
1594
- returned:
1595
-
1596
- ```typescript
1597
- import { step, z } from '@kici-dev/sdk';
1598
-
1599
- const configureNginx = step('configure-nginx', {
1600
- // optional schema for the drift value — gives the dashboard a typed shape
1601
- drift: z.object({ want: z.string() }),
1602
-
1603
- // read-only inspection; return null when already in the desired state
1604
- check: async (ctx) => {
1605
- const current = await ctx.$`nginx -T`;
1606
- return current.stdout.includes(DESIRED) ? null : { want: DESIRED };
1607
- },
1608
-
1609
- // human-readable preview line — REQUIRED when check is set. It is the drift's
1610
- // serializable face: it streams to the logs and persists for the dashboard.
1611
- summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
1612
-
1613
- // apply — runs only when check returned drift (apply mode); receives that drift
1614
- run: async (ctx, drift) => {
1615
- await writeConfig(drift.want);
1616
- return { reloaded: true };
1617
- },
1618
-
1619
- // optional — runs when check returned null, to produce the step's outputs
1620
- whenInSync: async () => ({ reloaded: false }),
1621
- });
1622
- ```
1623
-
1624
- ### The facet fields
1625
-
1626
- | Field | Required | Purpose |
1627
- | ------------ | ---------------- | ---------------------------------------------------------------------- |
1628
- | `check` | to opt in | Read-only inspection. Return a drift value, or `null` when in sync. |
1629
- | `summarize` | when `check` set | Human-readable, serializable preview of the drift. Streams + persists. |
1630
- | `run` | always | Apply function. With `check`, it receives the drift as its second arg. |
1631
- | `whenInSync` | optional | Produces the step's outputs when `check` returned `null`. |
1632
- | `drift` | optional | Schema that validates / shapes the drift value. |
1633
-
1634
- `summarize` is **required** whenever `check` is declared. `run` and `whenInSync`
1635
- both produce the same output type — one output shape per step, whichever path
1636
- runs. Every other step facet (`cache`, `rules`, `continueOnError`, `timeout`,
1637
- `retry`, `approval`, `onCancel`, `cleanup`, `outputs`) composes unchanged.
1638
-
1639
- A plain `step()` without `check` keeps its exact current behavior — the check
1640
- facet is fully optional.
1641
-
1642
- ## Run modes
1643
-
1644
- A run carries one of three modes:
1645
-
1646
- | Mode | CLI flags | Behavior |
1647
- | --------------------- | ------------------------- | ------------------------------------------------------------------------------------------ |
1648
- | `apply` | (default, no flags) | Converge: drift ⇒ apply ⇒ **applied**; null ⇒ **in sync** (skipped). |
1649
- | `check` | `--check` | Preview only: drift ⇒ **would change**; null ⇒ **in sync**. Never applies. Always exits 0. |
1650
- | `check-fail-on-drift` | `--check --fail-on-drift` | Same as check, but the run **fails** if any step reports drift. |
1651
-
1652
- Per-step outcomes:
1653
-
1654
- - **applied** — drift was found and the step applied the change (apply mode).
1655
- - **in sync** — `check` returned `null`; nothing to do.
1656
- - **would change** — drift was found in check mode; the change was previewed, not applied.
1657
- - **no check** — a plain step (no `check`) reached under check mode. A
1658
- side-effecting step can't be safely previewed, so it is skipped.
1659
-
1660
- In check mode KiCI never invokes a checked step's `run` (apply) — the preview is
1661
- guaranteed side-effect-free.
1662
-
1663
- ## Running in check mode
1664
-
1665
- `--check` and `--fail-on-drift` control drift reporting on `kici run remote`:
1666
-
1667
- ```bash
1668
- # Apply (default): converge the workflow.
1669
- kici run push --local
1670
- kici run remote my-fixture
1671
-
1672
- # Check: report drift, change nothing. Always exits 0.
1673
- kici run remote my-fixture --check
1674
-
1675
- # Check + fail on drift: fail the run when any step reports drift. Use this as a
1676
- # CI gate ("fail the build if prod has drifted").
1677
- kici run remote my-fixture --check --fail-on-drift
1678
- ```
1679
-
1680
- `--fail-on-drift` only modifies check mode — passing it without `--check` is an
1681
- error.
1682
-
1683
- ## Where outcomes show up
1684
-
1685
- A check-mode run is labeled in the dashboard with a **CHECK MODE — preview**
1686
- badge on the run header. Each step shows its outcome chip — applied / in sync /
1687
- would change / no check — and, when drift was detected, the `summarize` line
1688
- describing what would change. The rendering is read-only.
1689
-
1690
- ## See also
1691
-
1692
- - [Idempotent SDK helpers](https://docs.kici.dev/user/sdk/idempotent/) — the `idempotent()` / `idempotentStep()` convenience wrappers (always apply on drift), plus `checkStep()`, the clean-shape sibling that respects the run-level check mode.
1693
- - [Core SDK reference](https://docs.kici.dev/user/sdk/core/) — the `step()`, `job()`, and `workflow()` factories the check facet extends.
1694
- - [Lock file and drift](https://docs.kici.dev/user/lock-file-and-drift/) — how the lock file carries step capability flags.
1695
-
1696
- ---
1697
-
1698
691
  ## Private npm registries
1699
692
 
1700
693
  Source: https://docs.kici.dev/user/private-registries/
@@ -1744,7 +737,7 @@ export default workflow('build', {
1744
737
 
1745
738
  Per-field rules:
1746
739
 
1747
- - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/kici-admin-cli#allow-http-npm--permit-non-https-private-npm-registries)).
740
+ - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#allow-http-npm--permit-non-https-private-npm-registries)).
1748
741
  - **`scope`** — Optional. When present, the registry serves only that scope (`@my-org`). When absent, this entry becomes the **default** registry — at most one entry may omit `scope`.
1749
742
  - **`tokenSecret`** — Mandatory `<context>:<secret-name>`. The orchestrator looks up the secret in the named context via the per-context secret resolver. The bare name **must not** contain a colon.
1750
743
  - **`alwaysAuth`** — Defaults to `true`. Forces npm to send the token on every request (even GETs), which is what most managed-registry providers require.
@@ -1921,7 +914,7 @@ registries: [
1921
914
  ## Security model
1922
915
 
1923
916
  - **Per-context scoping.** Every `tokenSecret` and `installEnv` entry is qualified with a context name. The orchestrator runs the same protection-rule pipeline (branch / trust / concurrency / reviewer / wait-timer) against each named context **before** resolving any secret, so a workflow that wants a `production` token from a feature branch is rejected exactly like a job that tries to deploy to `production` from a feature branch. A reviewer-gated install context **pauses** the whole workflow dispatch as a workflow-scoped held run instead of resolving the token — see [Reviewer-gated installs](https://docs.kici.dev/user/private-registries/#reviewer-gated-installs) below.
1924
- - **Untrusted contributors get no tokens.** When a fork PR is dispatched and the contributor-trust resolution returns anything other than `trusted`, the orchestrator strips both `npmRegistries` and `installEnvSecrets` out of the dispatch. The install runs without auth and fails naturally on the first private dep fork PRs cannot ever observe a registry token, even if a misconfigured context lacks an explicit [minimum trust](https://docs.kici.dev/user/contexts/#minimum-trust) rule.
917
+ - **Untrusted refs get no tokens.** When the trust resolution returns anything other than `trusted` — every fork pull request does — the orchestrator strips both `npmRegistries` and `installEnvSecrets` out of the dispatch. The install runs without auth and fails naturally on the first private dep. A fork pull request cannot observe a registry token, even if a context lacks an explicit [minimum trust](https://docs.kici.dev/user/contexts/#minimum-trust) rule.
1925
918
  - **Lifecycle scripts disabled.** Whenever a private registry is in scope, the agent runs the install with `--ignore-scripts` (npm, pnpm, and yarn classic alike; yarn berry gets the equivalent `enableScripts: false`). A malicious `preinstall` / `postinstall` hook in committed `package.json` cannot read the synthesized token env vars, even though they exist in the install subprocess. For a pnpm or yarn workspace, the agent builds your in-repo dependency closure as a separate step **after** the install's auth is torn down, so build scripts never see the tokens either.
1926
919
  - **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
1927
920
  - **Job-scoped env-var names.** The synthesized auth env var is `KICI_NPM_TOKEN_<jobIdShort>_<i>` where `jobIdShort` is the first 8 chars of the dispatched job id. The name is unguessable from outside the install subprocess and not reused across jobs.
@@ -1958,7 +951,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
1958
951
 
1959
952
  - [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<context>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
1960
953
  - [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
1961
- - [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/kici-admin-cli#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
954
+ - [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
1962
955
 
1963
956
  ---
1964
957
 
@@ -2113,11 +1106,12 @@ commonly, an offline `{ issuer, jwks }` file for air-gapped checks.
2113
1106
 
2114
1107
  ### Why you supply it out-of-band
2115
1108
 
2116
- Given there's a single issuer, why pass it at all instead of letting the
2117
- verifier read it from the token? Because the issuer named **inside** a token
2118
- cannot be trusted: a forged bundle could carry a token that names
2119
- `iss: https://attacker.example` _and_ bundle a key set that "verifies" it,
2120
- making the whole signature chain circular and self-attesting. The verifier
1109
+ The verifier already resolves a sensible default, so why is naming the trust
1110
+ root a supported step at all -- why not let the verifier read the issuer from
1111
+ the token? Because the issuer named **inside** a token cannot be trusted. A
1112
+ forged bundle could carry a token that names `iss: https://attacker.example`
1113
+ _and_ bundle a key set that "verifies" it. That makes the whole signature chain
1114
+ circular and self-attesting. The verifier
2121
1115
  therefore pins to an issuer you supply out-of-band and checks the token against
2122
1116
  _that_ — the bundle is verified against a key set you trust, not one it shipped
2123
1117
  with. Naming the trust root is a security requirement, not a multiple-choice
@@ -2264,7 +1258,7 @@ KiCI provides an explicit secrets API that gives workflow steps controlled acces
2264
1258
 
2265
1259
  ## Overview
2266
1260
 
2267
- Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
1261
+ Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration/) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
2268
1262
 
2269
1263
  This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
2270
1264
 
@@ -2285,13 +1279,13 @@ Use whichever fits the workflow — most small teams stay on the dashboard; ops
2285
1279
 
2286
1280
  ### When the operator has disabled dashboard writes
2287
1281
 
2288
- The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy). When that flip is on:
1282
+ The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy/). When that flip is on:
2289
1283
 
2290
1284
  - The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Hovering or keyboard-focusing the lock shows a tooltip with the exact `kici-admin secret set` invocation needed. The control itself is inert, so there is nothing to click.
2291
1285
  - The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
2292
1286
  - `kici-admin secret set` becomes the single entry point for new and updated secret values.
2293
1287
 
2294
- This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, environment bindings).
1288
+ This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, context bindings).
2295
1289
 
2296
1290
  ### CLI input modes
2297
1291
 
@@ -2326,7 +1320,7 @@ Two cross-cutting flags help every mode:
2326
1320
 
2327
1321
  `kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
2328
1322
 
2329
- A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy#cli-input-modes-for-the-plaintext-path).
1323
+ A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy/#cli-input-modes-for-the-plaintext-path).
2330
1324
 
2331
1325
  ## Accessing secrets
2332
1326
 
@@ -2559,8 +1553,10 @@ Note that `get()` is async -- you must `await` the result.
2559
1553
 
2560
1554
  ## Typed secrets
2561
1555
 
2562
- When you run `kici types`, the compiler generates a `.kici/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
1556
+ When you run `kici types`, the compiler generates a `.kici/types/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
1557
+
1558
+ This file is a local development aid, not source: `kici init` gitignores `.kici/types/`, so each team member generates their own copy with `kici types` rather than committing it. When the Platform is unreachable, `kici types` keeps any existing copy untouched, or writes an empty stub if none exists, so type checking degrades to "no known keys" instead of failing.
2563
1559
 
2564
- See [CLI reference](https://docs.kici.dev/user/cli) for the `kici types` command.
1560
+ See [CLI reference](https://docs.kici.dev/user/cli/authoring-and-local/#kici-types) for the `kici types` command.
2565
1561
 
2566
1562
  ---