@kici-dev/compiler 0.5.0 → 0.6.1

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 (73) hide show
  1. package/dist/cli.js +46 -8
  2. package/dist/commands/approve.d.ts +12 -0
  3. package/dist/commands/approve.js +5 -2
  4. package/dist/commands/compile.js +4 -2
  5. package/dist/commands/doctor.js +2 -2
  6. package/dist/commands/endpoints.js +4 -6
  7. package/dist/commands/feedback.d.ts +53 -0
  8. package/dist/commands/feedback.js +142 -0
  9. package/dist/commands/held-run-client.d.ts +21 -1
  10. package/dist/commands/held-run-client.js +34 -15
  11. package/dist/commands/hook.js +22 -20
  12. package/dist/commands/index.d.ts +4 -0
  13. package/dist/commands/index.js +3 -1
  14. package/dist/commands/init.d.ts +9 -2
  15. package/dist/commands/init.js +43 -16
  16. package/dist/commands/login.js +1 -1
  17. package/dist/commands/orchestrators.js +3 -2
  18. package/dist/commands/reject.d.ts +12 -0
  19. package/dist/commands/reject.js +5 -2
  20. package/dist/commands/report/collect.d.ts +82 -0
  21. package/dist/commands/report/collect.js +234 -0
  22. package/dist/commands/report/identity.d.ts +48 -0
  23. package/dist/commands/report/identity.js +49 -0
  24. package/dist/commands/report/index.d.ts +63 -0
  25. package/dist/commands/report/index.js +119 -0
  26. package/dist/commands/report/upload.d.ts +38 -0
  27. package/dist/commands/report/upload.js +64 -0
  28. package/dist/commands/run-hold-watch.js +2 -2
  29. package/dist/commands/run.js +6 -3
  30. package/dist/commands/runs/show.js +80 -1
  31. package/dist/commands/types.js +51 -8
  32. package/dist/execution/sdk-alias.js +4 -2
  33. package/dist/fixtures/compiler.js +2 -1
  34. package/dist/format.js +3 -3
  35. package/dist/generators/secrets-dts.d.ts +8 -2
  36. package/dist/generators/secrets-dts.js +3 -2
  37. package/dist/hooks/installer.js +2 -1
  38. package/dist/llm-context/llms-architecture.txt +35 -13
  39. package/dist/llm-context/llms-cli-remote.txt +2347 -0
  40. package/dist/llm-context/llms-cli.txt +284 -2470
  41. package/dist/llm-context/llms-features-execution.txt +2028 -0
  42. package/dist/llm-context/llms-features.txt +298 -1483
  43. package/dist/llm-context/llms-full.txt +6153 -4639
  44. package/dist/llm-context/llms-getting-started.txt +292 -12
  45. package/dist/llm-context/llms-patterns.txt +176 -1
  46. package/dist/llm-context/llms-providers.txt +11 -27
  47. package/dist/llm-context/llms-sdk-runtime.txt +25 -4
  48. package/dist/llm-context/llms-sdk.txt +31 -1
  49. package/dist/llm-context/llms.txt +33 -18
  50. package/dist/local-plane/paths.d.ts +15 -0
  51. package/dist/local-plane/paths.js +22 -1
  52. package/dist/local-plane/plane-manager.js +2 -2
  53. package/dist/local-plane/port-holder.js +1 -1
  54. package/dist/local-plane/postgres.d.ts +3 -16
  55. package/dist/local-plane/postgres.js +10 -15
  56. package/dist/lockfile/generator.d.ts +12 -0
  57. package/dist/lockfile/generator.js +47 -14
  58. package/dist/postinstall.js +2 -1
  59. package/dist/remote/config.d.ts +2 -15
  60. package/dist/remote/config.js +2 -16
  61. package/dist/remote/dashboard-client.d.ts +39 -0
  62. package/dist/remote/dashboard-client.js +41 -0
  63. package/dist/remote/oauth.js +7 -5
  64. package/dist/remote/uploader.js +2 -2
  65. package/dist/templates/package-json.js +1 -1
  66. package/dist/test-runner/dry-run.js +4 -2
  67. package/dist/test-runner/git-detector.js +2 -1
  68. package/dist/test-runner/job-executor.js +2 -1
  69. package/dist/test-runner/payload-builder.js +11 -17
  70. package/dist/types.d.ts +33 -3
  71. package/dist/validation/validator.js +23 -6
  72. package/package.json +16 -11
  73. package/sbom.spdx.json +953 -901
@@ -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,1629 +254,437 @@ 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
 
253
- ### Inline approval and `--approve-all` in `kici run remote`
254
-
255
- 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.
256
-
257
- To auto-approve **every gate of the run you just dispatched**, pass `--approve-all` (alias `--yes`):
258
-
259
- ```bash
260
- kici run remote deploy-prod --approve-all
261
- ```
262
-
263
- `--approve-all` is **run-scoped** — it only auto-approves holds belonging to the run this invocation dispatched; there is no fleet-wide or account-wide auto-approve. Eligibility is still enforced per hold: if you are not eligible for a gate, that gate still blocks. Each auto-approved gate prints its payload before resolving and is recorded distinctly in the audit trail (`held_run.auto_approve`).
264
-
265
- `--approve-all` is honored in non-interactive runs too: with `--json` or `--quiet` (no TTY), the flag still auto-approves each eligible gate as it appears instead of hanging or printing out-of-band instructions. Hold notices are routed to stderr so `--json` stdout stays pure machine-readable output.
266
-
267
- You can also approve from the dashboard approval queue. See [Dashboard](https://docs.kici.dev/user/dashboard/contexts-and-secrets/#approval-queue).
268
-
269
- ## See also
270
-
271
- - [Idempotent steps](https://docs.kici.dev/user/idempotent-steps/) — the check/apply step facet that drift gates build on.
272
- - [Contexts](https://docs.kici.dev/user/contexts/) — operator-required reviewers on protected contexts.
273
- - [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) — teams, the approval queue, expiry, and self-approval.
274
- - [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. Job-level `concurrencyGroup` functions (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)) resolve the same way — on the agent's init step, never in the orchestrator.
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)_
486
-
487
- ---
488
-
489
- ## Contexts
490
-
491
- Source: https://docs.kici.dev/user/contexts/
492
-
493
- <!-- help:contexts-list#overview -->
494
-
495
- Contexts are named deployment targets (like staging or production) that control where your workflow jobs run. Each context can have its own variables, secrets, and protection rules to gate deployments.
496
-
497
- <!-- /help:contexts-list -->
498
-
499
- <!-- help:contexts-protection#protection-rules -->
500
-
501
- Protection rules control when jobs targeting a context can execute.
502
-
503
- Available rules:
504
-
505
- - **Branch restrictions** — only allow specific branches to deploy.
506
- - **Required reviewer approvals** — gate the run on human sign-off.
507
- - **Wait timers** — delay execution for a fixed period.
508
- - **Concurrency limits** — prevent collisions between parallel deployments.
509
-
510
- <!-- /help:contexts-protection -->
511
-
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
- ## Overview
515
-
516
- A context in KiCI provides:
517
-
518
- - **Variables** -- non-secret key-value configuration (e.g., `API_URL`, `CLUSTER_NAME`)
519
- - **Scoped secrets** -- encrypted values bound to the context via scope bindings
520
- - **Protection rules** -- branch restrictions, required reviewers, wait timers, and concurrency limits
521
- - **Per-source overrides** -- repositories can override unlocked variables for their own deployments
522
-
523
- ## SDK API
524
-
525
- ### Job-level context property
526
-
527
- The `context` property is set on a job, not a workflow or step:
528
-
529
- ```typescript
530
- import { workflow, job, step, push } from '@kici-dev/sdk';
531
-
532
- export default workflow('deploy', {
533
- on: [push({ branches: ['main'] })],
534
- jobs: [
535
- job('deploy-staging', {
536
- runsOn: 'default',
537
- context: 'staging',
538
- steps: [
539
- step('deploy', async (ctx) => {
540
- // ctx.context is the resolved context name
541
- console.log(`Deploying to ${ctx.context}`);
542
- // ctx.secrets provides async get/expose/has methods for context-bound secrets
543
- const dbPassword = await ctx.secrets.get('DB_PASSWORD');
544
- // Environment variables are in ctx.env
545
- const apiUrl = ctx.env.API_URL;
546
- await ctx.$`deploy --target ${ctx.context}`;
547
- }),
548
- ],
549
- }),
550
- ],
551
- });
552
- ```
553
-
554
- ### Dynamic contexts
555
-
556
- The context name can be a string or a function (sync or async) for dynamic contexts (e.g., per-PR review contexts). The function receives the normalized event envelope, with the raw provider body nested at `event.payload`:
557
-
558
- ```typescript
559
- job('deploy-review', {
560
- runsOn: 'default',
561
- context: (event) => `review/PR-${event.payload.pull_request.number}`,
562
- steps: [
563
- step('deploy', async (ctx) => {
564
- // ctx.context is 'review/PR-123' (resolved at runtime)
565
- await ctx.$`deploy-preview --env ${ctx.context}`;
566
- }),
567
- ],
568
- });
569
- ```
257
+ ### When a job is held twice
570
258
 
571
- 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.
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`.
572
260
 
573
- ### Multiple contexts per job
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`.
574
262
 
575
- A job can bind more than one context with `contexts`, an ordered array. This lets a single job draw secrets and variables from several contexts at once for example a shared `staging` context plus a `my-testing` context that carries test-only variables:
263
+ `--job` names both at once, so pick one with `--hold-type`:
576
264
 
577
- ```typescript
578
- job('deploy', {
579
- runsOn: 'default',
580
- contexts: ['staging', 'my-testing'],
581
- steps: [
582
- step('deploy', async (ctx) => {
583
- // ctx.secrets and ctx.env carry the merged set from both contexts
584
- const dbUrl = await ctx.secrets.get('DB_URL');
585
- }),
586
- ],
587
- });
265
+ ```bash
266
+ kici approve <run-id> --job deploy --hold-type reviewer
267
+ kici approve <run-id> --job deploy --hold-type security
588
268
  ```
589
269
 
590
- - `context` (singular) and `contexts` (array) are mutually exclusive setting both is a compile error. `context: 'staging'` is exactly equivalent to `contexts: ['staging']`.
591
- - Each array entry is a static name or a function of the event, resolved per element exactly like a single dynamic context.
592
-
593
- **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
-
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.
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.
596
271
 
597
- **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
-
599
- **Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context) simply adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic context resolving to an as-yet-unconfigured name does today.
272
+ Each hold carries its own expiry, so the job is cancelled when the **first** one runs out.
600
273
 
601
- **Registration rejects a provably-unsatisfiable binding.** When a workflow is registered, KiCI statically checks every multi-context binding: a bound context that does not exist, a disabled one, or two contexts with mutually-exclusive fixed branch / trigger-type / repository restrictions (no value can satisfy both) makes the binding provably unsatisfiable, and the registration is rejected with a precise message naming the job, the contexts, and the rule — for example `unsatisfiable context binding: job 'deploy' binds contexts [staging, my-testing] with mutually exclusive branch restrictions (no value satisfies all bound contexts)`. Bindings whose restrictions use globs are undecidable at registration and fall through to the dispatch-time gate check instead.
274
+ ### Inline approval and `--approve-all` in `kici run remote`
602
275
 
603
- ### Job-level environment variables
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.
604
277
 
605
- The `env` property on a job provides static or dynamic environment variables:
606
-
607
- ```typescript
608
- job('deploy', {
609
- runsOn: 'default',
610
- context: 'production',
611
- env: { DEPLOY_TARGET: 'us-east-1' },
612
- // Or dynamic:
613
- // env: (event) => ({ DEPLOY_SHA: event.payload.after?.slice(0, 7) }),
614
- steps: [
615
- step('deploy', async (ctx) => {
616
- // DEPLOY_TARGET is available in ctx.env
617
- await ctx.$`deploy --region ${ctx.env.DEPLOY_TARGET}`;
618
- }),
619
- ],
620
- });
621
- ```
622
-
623
- ### Concurrency groups
624
-
625
- Jobs can define their own concurrency groups to control concurrent execution within a context. For workflow-level concurrency (which applies to all jobs in a workflow), see [Concurrency groups](https://docs.kici.dev/user/concurrency/).
626
-
627
- Control concurrent deployments to the same context:
628
-
629
- ```typescript
630
- job('deploy', {
631
- runsOn: 'default',
632
- context: 'production',
633
- concurrencyGroup: 'production-api',
634
- // Or dynamic:
635
- // concurrencyGroup: (event) => `review-${event.payload.pull_request.number}`,
636
- steps: [/* ... */],
637
- });
638
- ```
639
-
640
- If no `concurrencyGroup` is specified, the context name is used as the default concurrency group. For a job bound to multiple contexts, the default is the **first** bound context's name.
641
-
642
- ### Step context
643
-
644
- Inside a step, the `ctx` object provides:
645
-
646
- | Property | Type | Description |
647
- | ------------- | ------------------------------------- | ----------------------------------------------------------------------------------------- |
648
- | `ctx.context` | `string \| undefined` | Resolved context name (undefined for jobs without context) |
649
- | `ctx.env` | `Record<string, string \| undefined>` | Environment variables (merged from system, org, source, and job-level `env`) |
650
- | `ctx.secrets` | `StepSecretsTyped` | Async accessor for bound secrets (get, expose, has, getMeta, list, mountFile, exposeFile) |
651
-
652
- | Method | Returns | Description |
653
- | -------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
654
- | `await ctx.secrets.get(key)` | `string` | Retrieve a secret value. Throws `SecretNotFoundError` if not found. |
655
- | `await ctx.secrets.expose(key)` | `void` | Set the secret as an environment variable for this step — visible via `ctx.env` and to child processes (`process.env`). Throws `SecretNotFoundError` if not found. |
656
- | `ctx.secrets.has(key)` | `boolean` | Check if a secret key exists. Synchronous, never throws. |
657
- | `ctx.secrets.getMeta(key)` | `SecretMeta \| undefined` | Retrieve metadata (value, backend name, scope path) for a resolved secret. Returns `undefined` if not found. |
658
- | `ctx.secrets.list()` | `string[]` | Every secret key available to the step, sorted alphabetically. Synchronous, never throws. |
659
- | `await ctx.secrets.mountFile(opts)` | `{ path }` | Materialise one or more secrets to a per-step tmpfile (auto-removed at step end). See [Secrets → Mounting secrets as files](https://docs.kici.dev/user/secrets/#mounting-secrets-as-files). |
660
- | `await ctx.secrets.exposeFile(envVar, opts)` | `{ path }` | `mountFile` plus `process.env[envVar] = path`; the env var is unset at step end. |
661
- | `ctx.setSecretOutput(key, val)` | `void` | Publish an encrypted secret output from this job, consumable by downstream jobs via `needs`. Never logged or stored in plaintext. |
662
-
663
- The full secrets API — including `SecretFileOptions`, log masking, and the canonical `sops` example — is documented in [Secrets](https://docs.kici.dev/user/secrets/).
664
-
665
- ## Environment variable merge precedence
666
-
667
- When a job targets a context, variables are merged in this order (last wins):
668
-
669
- 1. **Allowed system vars** -- `PATH`, `HOME`, etc. from the agent process
670
- 2. **Sandbox defaults** -- `FORCE_COLOR=1`
671
- 3. **KICI\_\* system vars** -- orchestrator-generated metadata
672
- 4. **Org-level context vars** -- from the dashboard, managed per-context
673
- 5. **Source-level overrides** -- per-repository overrides (skips locked vars)
674
- 6. **Job env** -- from the `env` property in the SDK
675
- 7. **`setEnv()` calls** -- runtime modifications within steps
676
-
677
- > **Note:** Secrets are NOT part of the environment variable merge. They are delivered to the step context via IPC and accessed through `ctx.secrets`, not through `process.env`. See the [step context](https://docs.kici.dev/user/contexts/#step-context) section above.
678
-
679
- ## Protection rules
680
-
681
- Contexts can have protection rules that gate job execution:
682
-
683
- ### Branch restrictions
684
-
685
- Limit which branches can deploy to a context:
686
-
687
- ```
688
- Allowed branches: main, release/*
689
- ```
690
-
691
- Jobs from other branches are rejected immediately with an error message.
692
-
693
- ### Required reviewers
694
-
695
- Require manual approval before a job can proceed:
696
-
697
- ```
698
- Required reviewers: alice, bob
699
- ```
700
-
701
- When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-approve) command, or the API. Held runs expire after a configurable timeout.
702
-
703
- This operator-set rule is the **mandatory** form of an approval gate. Workflow authors can also declare gates in code with `approval` at step, job, or workflow level — see [Approval gates](https://docs.kici.dev/user/approvals/). Both forms use the same held-element mechanism and the same queue.
704
-
705
- ### Wait timer
706
-
707
- Add a mandatory delay before deployment starts:
708
-
709
- ```
710
- Wait timer: 300 seconds
711
- ```
712
-
713
- The job waits for the specified duration before proceeding. Useful for staged rollouts.
714
-
715
- ### Minimum trust
716
-
717
- Gate job execution based on the contributor's trust tier for PR-triggered runs:
718
-
719
- ```
720
- Minimum trust: known
721
- ```
722
-
723
- | Value | Effect |
724
- | --------- | --------------------------------------------------- |
725
- | `known` | Blocks unknown contributors; allows known + trusted |
726
- | `trusted` | Blocks unknown + known; allows only trusted |
727
-
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.
729
-
730
- Trust tier is determined by the contributor's identity link and CI trust RBAC level:
731
-
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
735
-
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.
737
-
738
- See the [CI security architecture docs](https://docs.kici.dev/architecture/security/ci-security/) for the full trust resolution flow.
739
-
740
- ### Security approval queue
741
-
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.
743
-
744
- Held runs can be approved:
745
-
746
- - Via the **dashboard** in Settings > CI trust > Approval queue
747
- - Via a PR comment: `/kici approve` (commenter must have `ci_trust:write+`)
748
-
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).
750
-
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.
752
-
753
- ### Concurrency limits
754
-
755
- Control how many jobs can run simultaneously in a context:
756
-
757
- ```
758
- Concurrency limit: 1
759
- Strategy: queue (or cancel-pending)
760
- ```
761
-
762
- The concurrency limit is a positive integer; leave it unset for unlimited concurrency.
763
-
764
- - **queue** -- new jobs wait in a FIFO queue (with configurable timeout, default 1 hour)
765
- - **cancel-pending** -- pending (queued) jobs are cancelled when the limit is reached
766
-
767
- ## Dashboard management
768
-
769
- ### Creating contexts
770
-
771
- Navigate to **Settings > Contexts** in the dashboard. Click **New context** to choose the context name and type (Fixed or Glob).
772
-
773
- - **Fixed** -- applies to jobs that declare exactly this context name, like `staging` or `production`
774
- - **Glob** -- applies to any context name a job declares that matches the pattern, e.g. `review/*` matches a job with `context: 'review/PR-123'`
775
-
776
- The contexts list shows each context's type, whether test runs may use it (the `allowLocalExecution` flag -- see the [testing guide](https://docs.kici.dev/user/testing-guide/)), and whether it is enabled.
777
-
778
- ### Context detail page
779
-
780
- Each context has four tabs:
781
-
782
- 1. **Variables** -- manage key-value pairs with lock toggles. Locked variables cannot be overridden by source-level overrides. Source overrides are managed in a sub-tab.
783
-
784
- 2. **Secrets** -- view bound secret scopes and their resolved secret count. Add bindings by specifying scope glob patterns (e.g., `aws/prod/**`).
785
-
786
- 3. **Protection** -- configure branch restrictions, required reviewers, wait timers, and concurrency limits with enable toggles for each section. Turning a section's toggle off and saving clears that rule on the context, so the gate stops applying to new runs. Emptying the hold expiry field clears it too, and held runs fall back to the default one-hour hold window.
787
-
788
- 4. **History** -- view filtered runs targeting this context.
789
-
790
- ### Bound contexts on runs
791
-
792
- 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
-
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>`).
795
-
796
- ### Secrets management
797
-
798
- Secrets are individual encrypted values organized by scope paths (e.g., `aws/prod`, `databases/postgres`). Scopes are bound to contexts via bindings:
799
-
800
- - **Scope-centric view** (Secrets page): tree view of scopes with per-scope context binding checkboxes
801
- - **Context-centric view** (inside context detail): bound scopes, resolved secrets, add binding
802
-
803
- When scope paths collide on the same key name, the longer (more specific) path wins.
804
-
805
- ## Type generation
806
-
807
- Running `kici types` generates two augmented interfaces: `KnownSecretKeys` (union of all secret keys across all contexts) and `ContextSecrets` (per-context key unions):
808
-
809
- ```typescript
810
- interface KnownSecretKeys {
811
- DB_PASSWORD: string;
812
- API_KEY: string;
813
- }
814
-
815
- interface ContextSecrets {
816
- production: 'DB_PASSWORD' | 'API_KEY';
817
- staging: 'DB_PASSWORD';
818
- }
819
- ```
820
-
821
- `KnownSecretKeys` narrows `ctx.secrets.get()` and `ctx.secrets.expose()` key parameters to valid key names. `ContextSecrets` maps each context to its available secret key names as a string union. Dynamic contexts fall back to the full `KnownSecretKeys` union.
822
-
823
- ---
824
-
825
- ## Dashboard
826
-
827
- Source: https://docs.kici.dev/user/dashboard/
828
-
829
- The KiCI dashboard is the browser interface for monitoring workflow runs, inspecting jobs and logs, and managing your organization. It signs in via OIDC and talks to KiCI over its API.
830
-
831
- This guide is split across the following pages:
832
-
833
- | Page | Covers |
834
- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
835
- | [Getting started](https://docs.kici.dev/user/dashboard/getting-started/) | Onboarding checklist and your organizations list |
836
- | [Navigation and layout](https://docs.kici.dev/user/dashboard/navigation/) | Sidebar, mobile nav, theme, time display, shortcuts, error pages |
837
- | [Runs and logs](https://docs.kici.dev/user/dashboard/runs/) | Run list, run detail, the log viewer |
838
- | [Settings](https://docs.kici.dev/user/dashboard/settings/) | Members, roles, teams, keys, sources, billing, security, support access |
839
- | [Workflows, diagnostics, and orchestrators](https://docs.kici.dev/user/dashboard/workflows-and-diagnostics/) | Registered workflows, infra health, per-cluster views |
840
- | [Contexts, secrets, and approvals](https://docs.kici.dev/user/dashboard/contexts-and-secrets/) | Contexts, secret scopes, approval queue |
841
- | [Activity and DLQ](https://docs.kici.dev/user/dashboard/activity-and-dlq/) | Forensic activity log and dead-letter queue |
842
- | [Notifications](https://docs.kici.dev/user/dashboard/notifications/) | Personal run notifications and org-wide channels, subscriptions, delivery log |
843
- | [Account](https://docs.kici.dev/user/dashboard/account/) | Personal account settings |
844
-
845
- ---
846
-
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, it is resolved on the eval agent as a short **init** step that runs before the job:
875
-
876
- 1. The orchestrator dispatches a lightweight `__init__` job to an agent.
877
- 2. The agent loads the compiled workflow bundle and calls your function with the normalized event.
878
- 3. The agent reports the resolved values back to the orchestrator, which dispatches the real execution job with them applied.
879
-
880
- This resolution appears in the run timeline as an `Init:` entry. The orchestrator never evaluates workflow code — every dynamic `context`, `env`, and `concurrencyGroup` function runs agent-side, whatever it references.
881
-
882
- `kici preview` lists the injected `__init__` job under each affected job, so you can spot it before the first run.
883
-
884
- **Examples:**
885
-
886
- ```typescript
887
- // Simple branch extraction
888
- context: (event) => event.targetBranch;
889
-
890
- // Object literal with string operations
891
- env: (event) => ({ BRANCH: event.targetBranch });
892
-
893
- // Concatenation with event data
894
- concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
895
-
896
- // Local variables and safe globals
897
- context: (event) => {
898
- const parts = event.targetBranch.split('/');
899
- return parts[parts.length - 1];
900
- };
901
-
902
- // Async lookups, module access, and process/global reads all work
903
- context: async (event) => await lookupEnv(event.targetBranch);
904
- env: (event) => ({ DEFAULT_ENV: process.env.DEFAULT_ENV ?? 'staging' });
905
- ```
906
-
907
- ## Performance
908
-
909
- | Value | Overhead | Example |
910
- | ------------------------ | --------- | ---------------------------------------- |
911
- | Static value | None | `context: 'staging'` |
912
- | Dynamic value (function) | Init step | `context: (event) => event.targetBranch` |
913
-
914
- A static value is baked into the lock file and needs no init step. A dynamic value always resolves through the agent's init step, so reach for a function only when the value genuinely depends on the event.
915
-
916
- ## Tips
917
-
918
- - **Prefer static values when you can.** Most context and env values are the same on every event; only make them dynamic when they truly depend on the event payload.
919
- - **Run `kici preview`** to see the injected `__init__` job listed under each affected job before your first run.
920
- - **A runtime error in a dynamic function fails the job.** If your function throws when the init step runs it (e.g., accessing a property on `undefined`), the job fails immediately.
921
- - **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where dynamic values run relative to rules, hooks, and step bodies.
922
- - **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`, …).
923
-
924
- ---
925
-
926
- ## Environment variables
927
-
928
- Source: https://docs.kici.dev/user/env-vars/
929
-
930
- 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.
931
-
932
- ## Authentication
933
-
934
- | Variable | Description | Default |
935
- | --------------------- | -------------------------------------- | -------------------------------------------- |
936
- | `KICI_OIDC_ISSUER` | OIDC issuer URL for authentication | `https://auth.kici.dev/realms/kici-internal` |
937
- | `KICI_OIDC_CLIENT_ID` | OIDC client ID for the CLI application | `kici-cli` |
938
- | `KICI_PLATFORM_URL` | Platform API base URL | `https://api.kici.dev` |
939
- | `KICI_CONFIG_DIR` | Override the KiCI config directory | `~/.kici` |
940
-
941
- ## Browser behavior
942
-
943
- | Variable | Description | Default |
944
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
945
- | `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 |
946
- | `KICI_CALLBACK_PORT` | Fixed port for the OAuth PKCE callback server. Useful when firewall rules require a known port. | Random available port |
947
-
948
- ## Development
949
-
950
- | Variable | Description | Default |
951
- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
952
- | `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 |
953
- | `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 |
954
-
955
- ## Local dev plane
956
-
957
- Read by the [local dev plane](https://docs.kici.dev/user/cli/authoring-and-local/#kici-local) that `kici run <event> --local` dispatches through.
958
-
959
- | Variable | Description | Default |
960
- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
961
- | `KICI_LOCAL_ORCH_PORT` | Port the plane orchestrator listens on (HTTP + WebSocket). Change it when another process already holds the default. | `4319` |
962
- | `KICI_LOCAL_PG_PORT` | Port the plane's PostgreSQL listens on. | `45432` |
963
- | `KICI_LOCAL_PG_MODE` | Set to `podman` to force the container PostgreSQL fallback instead of the embedded binary. | Embedded PostgreSQL |
964
- | `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) |
965
-
966
- ## CI detection
967
-
968
- The CLI also reads the conventional CI markers your CI provider sets. They are not KiCI variables — KiCI only consumes them.
969
-
970
- | Variable | Description | Default |
971
- | ---------------- | -------------------------------------------- | ------- |
972
- | `CI` | Generic CI marker. Set by most CI providers. | unset |
973
- | `GITHUB_ACTIONS` | Set to `true` by GitHub Actions. | unset |
974
- | `GITLAB_CI` | Set to `true` by GitLab CI/CD. | unset |
975
-
976
- ### How `CI` is interpreted
977
-
978
- `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.
979
-
980
- 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.
981
-
982
- This affects which login flow `kici login` chooses (browser vs device) and whether interactive commands such as `kici init` prompt.
983
-
984
- ## Usage examples
985
-
986
- ### CI/CD environment
987
-
988
- Authenticate with a pre-existing API key (no browser needed):
278
+ To auto-approve **every gate of the run you just dispatched**, pass `--approve-all` (alias `--yes`):
989
279
 
990
280
  ```bash
991
- kici login --token "$KICI_API_KEY"
992
- ```
993
-
994
- ### Targeting another environment or custom OIDC provider
995
-
996
- `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:
997
-
998
- ```bash
999
- export KICI_OIDC_ISSUER=https://your-idp.example.com
1000
- export KICI_OIDC_CLIENT_ID=your-client-id
1001
- export KICI_PLATFORM_URL=https://your-platform.example.com
1002
- kici login
1003
- ```
1004
-
1005
- ### Headless SSH session
1006
-
1007
- The CLI auto-detects headless environments and uses the device flow. To force PKCE with URL output instead:
1008
-
1009
- ```bash
1010
- export KICI_BROWSER_CMD=none
1011
- kici login
1012
- ```
1013
-
1014
- This prints the authorization URL to stdout as `KICI_AUTH_URL=<url>`. Open the URL in any browser to complete authentication.
1015
-
1016
- ### Fixed callback port
1017
-
1018
- When behind a firewall or using port forwarding:
1019
-
1020
- ```bash
1021
- export KICI_CALLBACK_PORT=19876
1022
- kici login
1023
- ```
1024
-
1025
- ### Custom config location
1026
-
1027
- Store the KiCI config in a non-default location:
1028
-
1029
- ```bash
1030
- export KICI_CONFIG_DIR=/tmp/kici-test
1031
- kici login
1032
- ```
1033
-
1034
- ---
1035
-
1036
- ## Event system
1037
-
1038
- Source: https://docs.kici.dev/user/events/
1039
-
1040
- 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.
1041
-
1042
- ## Overview
1043
-
1044
- 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.
1045
-
1046
- 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:
1047
-
1048
- - `kiciEvent()` -- custom events emitted from workflow steps
1049
- - `workflowComplete()` -- fires when a workflow finishes
1050
- - `jobComplete()` -- fires when a specific job finishes
1051
- - `genericWebhook()` -- HTTP webhooks from external services
1052
- - `schedule()` -- cron-based time triggers
1053
- - `lifecycle()` -- orchestrator lifecycle events (workflow completion, job failure, registration updates)
1054
-
1055
- All six require the **registration model** to function -- covered in detail below.
1056
-
1057
- ## Event types
1058
-
1059
- ### Custom events
1060
-
1061
- Custom events are user-defined events emitted from workflow steps using `ctx.emit()`. Use `kiciEvent()` to listen for them.
1062
-
1063
- ```typescript
1064
- import { kiciEvent } from '@kici-dev/sdk';
1065
-
1066
- // Listen for a custom event by name
1067
- kiciEvent({ name: 'deploy-complete' });
1068
-
1069
- // With payload matching (JSONPath)
1070
- kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } });
1071
-
1072
- // With negative filter
1073
- kiciEvent({ name: 'deploy-complete', not: { '$.env': 'staging' } });
1074
-
1075
- // From a specific repository
1076
- kiciEvent({ name: 'deploy-complete', source: 'org/infra-repo' });
1077
- ```
1078
-
1079
- **Config options:** `name` (required), `match`, `not`, `source`, `description`.
1080
-
1081
- ### System events
1082
-
1083
- The orchestrator automatically emits completion events when workflows and jobs finish. No manual emission needed -- these fire automatically.
1084
-
1085
- **Workflow completion:**
1086
-
1087
- ```typescript
1088
- import { workflowComplete } from '@kici-dev/sdk';
1089
-
1090
- // Any workflow completion
1091
- workflowComplete();
1092
-
1093
- // Specific workflow by name
1094
- workflowComplete({ name: 'build' });
1095
-
1096
- // Only successful completions
1097
- workflowComplete({ name: 'build', status: ['success'] });
1098
- ```
1099
-
1100
- **Config options:** `name`, `status` (`'success'`, `'failed'`, `'cancelled'`), `source`, `description`.
1101
-
1102
- **Job completion:**
1103
-
1104
- ```typescript
1105
- import { jobComplete } from '@kici-dev/sdk';
1106
-
1107
- // Any job completion
1108
- jobComplete();
1109
-
1110
- // Specific workflow + job
1111
- jobComplete({ workflow: 'build', job: 'test' });
1112
-
1113
- // Only failures
1114
- jobComplete({ workflow: 'build', job: 'test', status: ['failed'] });
1115
- ```
1116
-
1117
- **Config options:** `workflow`, `job`, `status` (`'success'`, `'failed'`, `'cancelled'`, `'skipped'`), `source`, `description`.
1118
-
1119
- ### External events
1120
-
1121
- Generic webhooks let you trigger workflows from any HTTP service -- Stripe, ArgoCD, Slack, Grafana, or your own internal services.
1122
-
1123
- ```typescript
1124
- import { genericWebhook } from '@kici-dev/sdk';
1125
-
1126
- // Match any event from a source
1127
- genericWebhook({ source: 'stripe' });
1128
-
1129
- // Match specific event types
1130
- genericWebhook({ source: 'stripe', events: ['invoice.paid'] });
1131
-
1132
- // With HMAC-SHA256 signature verification
1133
- genericWebhook({
1134
- source: 'stripe',
1135
- events: ['invoice.paid'],
1136
- auth: {
1137
- method: 'hmac-sha256',
1138
- secret: 'stripe-signing-key',
1139
- signatureHeader: 'stripe-signature',
1140
- },
1141
- });
1142
-
1143
- // With API key auth
1144
- genericWebhook({
1145
- source: 'slack',
1146
- auth: { method: 'api-key', secret: 'slack-token' },
1147
- });
1148
- ```
1149
-
1150
- **Config options:** `source` (required), `events`, `match`, `not`, `auth`, `path`, `description`.
1151
-
1152
- 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.
1153
-
1154
- ### Schedule events
1155
-
1156
- Cron-based triggers evaluated by the orchestrator on a periodic interval. Only the Raft leader evaluates schedules in a clustered deployment.
1157
-
1158
- ```typescript
1159
- import { schedule } from '@kici-dev/sdk';
1160
-
1161
- // Run every hour
1162
- schedule({ cron: '0 * * * *' });
1163
-
1164
- // Run daily at 2 AM UTC
1165
- schedule({ cron: '0 2 * * *' });
1166
-
1167
- // Run weekly on Mondays at 9 AM Eastern
1168
- schedule({ cron: '0 9 * * 1', timezone: 'America/New_York' });
1169
- ```
1170
-
1171
- **Config options:** `cron` (required), `timezone` (defaults to `'UTC'`), `description`.
1172
-
1173
- ### Lifecycle events
1174
-
1175
- Lifecycle triggers listen for orchestrator-level events related to workflow execution and system state changes.
1176
-
1177
- ```typescript
1178
- import { lifecycle } from '@kici-dev/sdk';
1179
-
1180
- // Trigger when any workflow completes
1181
- lifecycle({ events: ['workflow_complete'] });
1182
-
1183
- // Trigger on job failures from a specific repo
1184
- lifecycle({ events: ['job_failed'], sources: ['org/deploy-repo'] });
1185
-
1186
- // Trigger when registrations are updated
1187
- lifecycle({ events: ['registration_updated'] });
1188
- ```
1189
-
1190
- **Available events:** `'workflow_complete'`, `'job_complete'`, `'job_failed'`, `'registration_updated'`.
1191
-
1192
- **Config options:** `events` (required), `sources`, `description`.
1193
-
1194
- ## The registration model
1195
-
1196
- This is the most important concept for understanding event-based triggers.
1197
-
1198
- ### Why registrations exist
1199
-
1200
- 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.
1201
-
1202
- 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.
1203
-
1204
- ### How registration works
1205
-
1206
- 1. You define a workflow with an event-based trigger (e.g., `schedule()`, `kiciEvent()`, `genericWebhook()`)
1207
- 2. You compile the workflow (`kici compile`), which produces a lock file
1208
- 3. You push the lock file to your repository's **default branch** (e.g., `main` or `master`)
1209
- 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
1210
- 5. Those workflows are stored in the orchestrator's registration database
1211
- 6. From that point on, matching events will trigger those workflows
1212
-
1213
- ### Key implications
1214
-
1215
- - **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.
1216
-
1217
- - **Registration is automatic.** There is no manual setup. Push your code, and the orchestrator handles the rest.
1218
-
1219
- - **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.
1220
-
1221
- - **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.
1222
-
1223
- ### Practical example
1224
-
1225
- You create a nightly build workflow:
1226
-
1227
- ```typescript
1228
- import { workflow, job, step, schedule } from '@kici-dev/sdk';
1229
-
1230
- export default workflow('nightly-build', {
1231
- on: schedule({ cron: '0 2 * * *' }),
1232
- jobs: [
1233
- job('build', {
1234
- runsOn: 'linux',
1235
- steps: [
1236
- step('build', async ({ $ }) => {
1237
- await $`pnpm build`;
1238
- }),
1239
- ],
1240
- }),
1241
- ],
1242
- });
1243
- ```
1244
-
1245
- 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.
1246
-
1247
- 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.
1248
-
1249
- ## How events are matched
1250
-
1251
- When an event arrives, the orchestrator follows this flow:
1252
-
1253
- 1. **Event received** -- a custom event is emitted by a step, a cron timer fires, or a generic webhook arrives
1254
- 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)
1255
- 3. **Trigger evaluation** -- for each candidate workflow, the orchestrator evaluates the trigger conditions: event name patterns, payload matching, status filters, source filters
1256
- 4. **Dispatch** -- matched workflows are dispatched to agents for execution, following the same job queue and agent routing as git-triggered workflows
1257
-
1258
- This lookup is fast because the registration index is held in memory and refreshed only when the registry version changes (on default-branch pushes).
1259
-
1260
- ### Cross-source webhook delivery
1261
-
1262
- 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.
1263
-
1264
- 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.
1265
-
1266
- ## Circuit breaker
1267
-
1268
- Events can trigger workflows that emit more events, creating chains. The circuit breaker prevents runaway event storms.
1269
-
1270
- ### Chain depth limit
1271
-
1272
- 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.
1273
-
1274
- - **Default limit:** 10 levels deep
1275
- - **What happens when hit:** the event is dropped and logged. It is not queued for later delivery.
1276
-
1277
- 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.
1278
-
1279
- ### Rate limiting
1280
-
1281
- Each workflow is rate-limited on how many events it can process per minute, using a sliding window.
1282
-
1283
- - **Default limit:** 100 events per workflow per minute
1284
- - **What happens when hit:** additional events for that workflow are dropped and logged until the window clears.
1285
-
1286
- These defaults are hardcoded in the orchestrator and are not currently configurable via environment variables.
1287
-
1288
- ## Delivery guarantees
1289
-
1290
- KiCI's event router delivers every accepted event with **at-least-once** semantics:
1291
-
1292
- - An event that passes the circuit breaker (chain depth + rate limit) and commits
1293
- to the `kici_events` table is guaranteed to dispatch to all matching workflows
1294
- at least once.
1295
- - Each dispatch attempt acquires a short-lived lease (default 60 s) on the row.
1296
- If the dispatching node crashes or the handler throws, the lease expires (or
1297
- is released on failure) and the event is automatically retried.
1298
- - The retry policy is exponential backoff with full jitter: base 5 s, cap 5 min,
1299
- up to 5 attempts before the event lands in the **DLQ** (dead-letter queue).
1300
- Operators triage DLQ entries via `kici-admin event-dlq list / count / retry / discard`.
1301
-
1302
- **What this means for workflow authors:**
1303
-
1304
- - **Make event handlers idempotent.** A retried dispatch may run a handler more
1305
- than once (e.g. if the first attempt threw after a partial side-effect).
1306
- Workflows that mutate external state should use idempotency keys, conditional
1307
- writes, or other deduplication patterns — same advice as for any distributed
1308
- CI system.
1309
- - **Schedule fires are at-least-once too.** A cron schedule that fires while a
1310
- leader is being killed will commit (atomically with `cron_last_fired`) or roll
1311
- back together — never half. Recovery on the new leader does not backfill
1312
- multiple missed instants; if your workflow needs at-least-N guarantees across
1313
- outages, drive it from a different mechanism (e.g. a workflow that runs more
1314
- frequently and emits its own custom event).
1315
- - **Drops are still possible — and visible.** Events rejected by the circuit
1316
- breaker (chain depth or rate limit exceeded) are dropped and logged, not
1317
- retried. That's a deliberate safety mechanism; the metric to watch is
1318
- `kici_orch_events_dropped_total{reason}`.
1319
-
1320
- ## Emitting custom events
1321
-
1322
- Custom events are emitted from workflow steps using `ctx.emit()`. You can optionally define typed event schemas using `defineEvent()`.
1323
-
1324
- ### Basic emission
1325
-
1326
- ```typescript
1327
- import { workflow, job, step, push } from '@kici-dev/sdk';
1328
-
1329
- export default workflow('build', {
1330
- on: push({ branches: 'main' }),
1331
- jobs: [
1332
- job('build', {
1333
- runsOn: 'linux',
1334
- steps: [
1335
- step('build', async ({ $ }) => {
1336
- await $`pnpm build`;
1337
- }),
1338
- step('notify', async (ctx) => {
1339
- await ctx.emit('build-complete', {
1340
- version: '1.0.0',
1341
- success: true,
1342
- });
1343
- }),
1344
- ],
1345
- }),
1346
- ],
1347
- });
1348
- ```
1349
-
1350
- ### Typed event definitions
1351
-
1352
- Use `defineEvent()` with Zod schemas to create a typed contract for event payloads:
1353
-
1354
- ```typescript
1355
- import { defineEvent, z } from '@kici-dev/sdk';
1356
-
1357
- export const deployComplete = defineEvent(
1358
- 'deploy-complete',
1359
- z.object({
1360
- env: z.string(),
1361
- version: z.string(),
1362
- services: z.array(z.string()),
1363
- }),
1364
- );
1365
- ```
1366
-
1367
- Then emit using the definition — the payload is checked against the schema:
1368
-
1369
- ```typescript
1370
- step('emit', async (ctx) => {
1371
- await ctx.emit(deployComplete, {
1372
- env: 'prod',
1373
- version: '1.2.3',
1374
- services: ['api', 'web'],
1375
- });
1376
- });
1377
- ```
1378
-
1379
- And consume in another workflow:
1380
-
1381
- ```typescript
1382
- import { workflow, job, step, kiciEvent } from '@kici-dev/sdk';
1383
-
1384
- export default workflow('post-deploy', {
1385
- on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }),
1386
- jobs: [
1387
- job('smoke-test', {
1388
- runsOn: 'linux',
1389
- steps: [
1390
- step('test', async ({ $ }) => {
1391
- await $`./scripts/smoke-test.sh`;
1392
- }),
1393
- ],
1394
- }),
1395
- ],
1396
- });
1397
- ```
1398
-
1399
- 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.
1400
-
1401
- ## See also
1402
-
1403
- - [SDK reference: event triggers](https://docs.kici.dev/user/sdk/triggers/#event-triggers) -- complete API signatures for all trigger builders
1404
- - [SDK reference: emitting events](https://docs.kici.dev/user/sdk/validation-events/#emitting-events) -- `ctx.emit()` and `defineEvent()` API
1405
- - [Workflow patterns: workflow chaining](https://docs.kici.dev/user/patterns/integrations/#workflow-chaining) -- examples of event-driven workflow chains
1406
- - [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) -- configuring generic webhook sources, trust relationships, and event routing
1407
- - [Architecture: event system](https://docs.kici.dev/architecture/webhooks/event-system/) -- internal event routing design, registration model, cluster synchronization
1408
-
1409
- ---
1410
-
1411
- ## Global workflows
1412
-
1413
- Source: https://docs.kici.dev/user/global-workflows/
1414
-
1415
- 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."
1416
-
1417
- 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.
1418
-
1419
- ## Mental model
1420
-
1421
- | Term | Meaning |
1422
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1423
- | Workflow repo | The repo whose `.kici/workflows/*.ts` file **declares** the global workflow. Holds the steps. Also known as the _authoring_ repo. |
1424
- | 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. |
1425
- | Global | A workflow whose trigger carries one or more `repos:` glob patterns. The presence of `repos:` is what classifies a workflow as global. |
1426
- | Authoring axis | Policy that answers "which repos may **author** global workflows?" Controlled by the allow-list in the dashboard's _Allowed author repos_ setting. |
1427
- | Source axis | Policy that answers "which **source** repos' events are allowed to trigger global workflows?" Controlled by the deny-list in _Blocked source repos_. |
281
+ kici run remote deploy-prod --approve-all
282
+ ```
1428
283
 
1429
- 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.
284
+ `--approve-all` is **run-scoped** it only auto-approves holds belonging to the run this invocation dispatched; there is no fleet-wide or account-wide auto-approve. Eligibility is still enforced per hold: if you are not eligible for a gate, that gate still blocks. Each auto-approved gate prints its payload before resolving and is recorded distinctly in the audit trail (`held_run.auto_approve`).
1430
285
 
1431
- ## Declaring a global workflow
286
+ `--approve-all` is honored in non-interactive runs too: with `--json` or `--quiet` (no TTY), the flag still auto-approves each eligible gate as it appears instead of hanging or printing out-of-band instructions. Hold notices are routed to stderr so `--json` stdout stays pure machine-readable output.
1432
287
 
1433
- Add `repos:` to any trigger. Any workflow with at least one `repos:`-bearing trigger becomes global automatically; no separate flag is required.
288
+ You can also approve from the dashboard approval queue. See [Dashboard](https://docs.kici.dev/user/dashboard/contexts-and-secrets/#approval-queue).
1434
289
 
1435
- ```ts
1436
- import { workflow, job, step, push } from '@kici-dev/sdk';
290
+ ## See also
1437
291
 
1438
- export default workflow('org-lint', {
1439
- on: [
1440
- push({
1441
- repos: ['myorg/*', '!myorg/archived-*'],
1442
- branches: ['main'],
1443
- }),
1444
- ],
1445
- jobs: [
1446
- job('lint', {
1447
- steps: [
1448
- step('lint-all', async ({ $, env }) => {
1449
- await $`echo source=${env.KICI_SOURCE_REPO_PATH ?? 'unknown'}`;
1450
- await $`npm run lint`;
1451
- }),
1452
- ],
1453
- }),
1454
- ],
1455
- });
1456
- ```
292
+ - [Idempotent steps](https://docs.kici.dev/user/idempotent-steps/) — the check/apply step facet that drift gates build on.
293
+ - [Contexts](https://docs.kici.dev/user/contexts/) — operator-required reviewers on protected contexts.
294
+ - [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) — teams, the approval queue, expiry, and self-approval.
295
+ - [Approval gates (architecture)](https://docs.kici.dev/architecture/approvals/) — the unified hold model and the step-level round-trip.
296
+ - [Organization-wide workflows](https://docs.kici.dev/user/global-workflows/) — why an approval gate is refused there.
1457
297
 
1458
- Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org, including one whose identifier starts with a dot (`.github/workflows-config`) — a repo identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own. Path globs in `paths:` keep the usual convention and do not match dot-prefixed files unless the pattern spells the dot out.
298
+ ---
1459
299
 
1460
- ### At a dual-repo checkout
300
+ ## Contexts
1461
301
 
1462
- The agent checks out both repos. **Inside a step body**, `env` carries a pointer to each working tree:
302
+ Source: https://docs.kici.dev/user/contexts/
1463
303
 
1464
- | `env` var | Points to |
1465
- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
1466
- | `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. |
1467
- | `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. |
304
+ <!-- help:contexts-list#overview -->
1468
305
 
1469
- Both variables are step-body context. They are **not** projected into the job's process environment, so a job-level `env:` block, a container image's entrypoint, or a shell command outside a step body will not see them. Outside a step body, use the `sourceRepo` / `workflowRepo` pair on the filter, generator, and rule contexts described below.
306
+ Contexts are named deployment targets (like staging or production) that control where your workflow jobs run. Each context can have its own variables, secrets, and protection rules to gate deployments.
1470
307
 
1471
- They are also set **only when there are two repos to point at**. An event from the workflow's own repo is matched from that repo's lock file rather than as a global candidate, so the workflow runs as an ordinary single-repo workflow: one checkout, and neither variable set. Read them with a fallback, as the example above does.
308
+ <!-- /help:contexts-list -->
1472
309
 
1473
- A global workflow's job runs with **no secrets at all** — neither the source repo's nor its own. See _Secrets are not available_ below.
310
+ <!-- help:contexts-protection#protection-rules -->
1474
311
 
1475
- ### The triggering event
312
+ Protection rules control when jobs targeting a context can execute.
1476
313
 
1477
- `ctx.event` inside a global workflow's job is the **source** repo's normalized event — the push or PR that fired the workflow, from a repo the workflow's own author may not own. `ctx.event.sourceRepo` names that repo.
314
+ Available rules:
1478
315
 
1479
- That field is what makes a per-source-repo concurrency group expressible and you have to write it. A global workflow runs on events from many repos, and their default branches share a name, so a group keyed on the branch alone puts every repo in one group, and with `cancelInProgress` (the default) one repo's push cancels another repo's in-flight run. That is still the behaviour of a branch-only group; naming the source repo in the key is what separates them:
316
+ - **Branch restrictions**only allow specific branches to deploy.
317
+ - **Required reviewer approvals** — gate the run on human sign-off.
318
+ - **Wait timers** — delay execution for a fixed period.
319
+ - **Concurrency limits** — prevent collisions between parallel deployments.
1480
320
 
1481
- ```ts
1482
- concurrency: {
1483
- group: ({ branch, event }) => `${event.sourceRepo}:${branch}`,
1484
- cancelInProgress: true,
1485
- },
1486
- ```
321
+ <!-- /help:contexts-protection -->
1487
322
 
1488
- ### Narrowing to the repos that need it
323
+ ## Overview
1489
324
 
1490
- A global workflow that matches `myorg/*` will, by default, run on every repo in the org. Three mechanisms narrow it to the repos it actually applies to, in increasing order of power:
325
+ A context in KiCI provides:
1491
326
 
1492
- 1. **A `requires` content filter on the trigger** the cheapest gate. The orchestrator checks a file's contents (a JSON-path probe over `package.json`, for example) and drops the workflow **before any agent is dispatched** when the condition is not met. See [`requires` on triggers](https://docs.kici.dev/user/sdk/triggers/#content-requirements-requires). This is provider-dependent — it needs a file-contents fetcher, which the GitHub provider supplies.
1493
- 2. **A workflow-level `filter` predicate** arbitrary TypeScript over the checked-out source tree (below). Works with any provider that clones.
1494
- 3. **A `DynamicJobFn`** generate the exact job set from the source repo's state ([Generating jobs per source repo](https://docs.kici.dev/user/global-workflows/#generating-jobs-per-source-repo) below).
327
+ - **Variables** -- non-secret key-value configuration (e.g., `API_URL`, `CLUSTER_NAME`)
328
+ - **Scoped secrets** -- encrypted values bound to the context via scope bindings
329
+ - **Protection rules** -- branch restrictions, required reviewers, wait timers, and concurrency limits
330
+ - **Per-source overrides** -- repositories can override unlocked variables for their own deployments
1495
331
 
1496
- ### Narrowing with a filter
332
+ ## SDK API
1497
333
 
1498
- Before reaching for a `filter`, check whether a declarative filter answers the question. `commitMessage` (on the trigger) and `requires` (over source files) are evaluated by the orchestrator from data it already has, so they cost no evaluation job at all — while a `filter` predicate dispatches one per (event × workflow repo). Gating on a `[skip ci]` marker, a conventional-commit prefix, or the contents of a named config file needs no predicate.
334
+ ### Job-level context property
1499
335
 
1500
- A workflow can declare a `filter`: a predicate that decides whether the workflow applies to this event at all.
336
+ The `context` property is set on a job, not a workflow or step:
1501
337
 
1502
- ```ts
338
+ ```typescript
1503
339
  import { workflow, job, step, push } from '@kici-dev/sdk';
1504
340
 
1505
- export default workflow('org-container-lint', {
1506
- on: [push({ repos: ['myorg/*'] })],
1507
- filter: async ({ sourceRepo, changedFilesStatus, $ }) => {
1508
- // `changedFiles` throws when the diff is unavailable, so guard first.
1509
- if (changedFilesStatus !== 'fetched') return true;
1510
- const found = await $`ls ${sourceRepo.path}`;
1511
- return found.stdout.includes('Dockerfile');
1512
- },
341
+ export default workflow('deploy', {
342
+ on: [push({ branches: ['main'] })],
1513
343
  jobs: [
1514
- job('lint-dockerfile', {
1515
- runsOn: ['kici:os:linux'],
344
+ job('deploy-staging', {
345
+ runsOn: 'default',
346
+ context: 'staging',
1516
347
  steps: [
1517
- step('lint', async ({ $, env }) => $`hadolint ${env.KICI_SOURCE_REPO_PATH}/Dockerfile`),
348
+ step('deploy', async (ctx) => {
349
+ // ctx.context is the resolved context name
350
+ console.log(`Deploying to ${ctx.context}`);
351
+ // ctx.secrets provides async get/expose/has methods for context-bound secrets
352
+ const dbPassword = await ctx.secrets.get('DB_PASSWORD');
353
+ // Environment variables are in ctx.env
354
+ const apiUrl = ctx.env.API_URL;
355
+ await ctx.$`deploy --target ${ctx.context}`;
356
+ }),
1518
357
  ],
1519
358
  }),
1520
359
  ],
1521
360
  });
1522
361
  ```
1523
362
 
1524
- The filter receives a `FilterContext`:
363
+ ### Dynamic contexts
364
+
365
+ The context name can be a string or a function (sync or async) for dynamic contexts (e.g., per-PR review contexts). The function receives the normalized event envelope, with the raw provider body nested at `event.payload`:
1525
366
 
1526
- | Property | Type | Description |
1527
- | -------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------ |
1528
- | `sourceRepo` | `RepoInfo` | The repo whose event triggered this evaluation, checked out on the evaluating agent. |
1529
- | `workflowRepo` | `RepoInfo` | The repo that registered the workflow. Identical to `sourceRepo` for a same-repo workflow. |
1530
- | `event` | `EventPayload` | The normalized event envelope. |
1531
- | `changedFiles` | `string[]` | Files changed in this event. Throws when unavailable — guard with `changedFilesStatus`. |
1532
- | `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` can be read. |
1533
- | `env` | `Record<string, string\|undefined>` | Environment variables. |
1534
- | `$` | zx shell | Shell executor. |
367
+ ```typescript
368
+ job('deploy-review', {
369
+ runsOn: 'default',
370
+ context: (event) => `review/PR-${event.payload.pull_request.number}`,
371
+ steps: [
372
+ step('deploy', async (ctx) => {
373
+ // ctx.context is 'review/PR-123' (resolved at runtime)
374
+ await ctx.$`deploy-preview --env ${ctx.context}`;
375
+ }),
376
+ ],
377
+ });
378
+ ```
1535
379
 
1536
- `RepoInfo` carries `path` (an absolute path to the checkout on the evaluating agent) plus optional `ref` and `sha`. **Both are optional** an event that carries no single ref leaves them undefined, so guard before reading them.
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.
1537
381
 
1538
- **`sourceRepo.path` is not stable across evaluations.** Its _contents_ are: the evaluating agent and the later run see the same tree at the same commit. The path itself is not — a different working directory, and possibly a different machine. Read _through_ it; never embed it in a job name, an output, or anything compared across calls.
382
+ ### Multiple contexts per job
1539
383
 
1540
- **A `filter` must be pure and deterministic.** Decide from the context alone the event, the changed files, and the checked-out treeso the same event always yields the same verdict.
384
+ A job can bind more than one context with `contexts`, an ordered array. This lets a single job draw secrets and variables from several contexts at once for example a shared `staging` context plus a `my-testing` context that carries test-only variables:
1541
385
 
1542
- ### Global and same-repo filters differ
386
+ ```typescript
387
+ job('deploy', {
388
+ runsOn: 'default',
389
+ contexts: ['staging', 'my-testing'],
390
+ steps: [
391
+ step('deploy', async (ctx) => {
392
+ // ctx.secrets and ctx.env carry the merged set from both contexts
393
+ const dbUrl = await ctx.secrets.get('DB_URL');
394
+ }),
395
+ ],
396
+ });
397
+ ```
1543
398
 
1544
- The same `filter` keyword means two different things depending on whether the workflow is global:
399
+ - `context` (singular) and `contexts` (array) are mutually exclusive setting both is a compile error. `context: 'staging'` is exactly equivalent to `contexts: ['staging']`.
400
+ - Each array entry is a static name or a function of the event, resolved per element exactly like a single dynamic context.
1545
401
 
1546
- | | Global workflow (`repos:` on a trigger) | Same-repo workflow |
1547
- | ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
1548
- | Evaluated | once per (event × workflow repo) | once per job that reaches dispatch, and once per job generator |
1549
- | Evaluated relative to the run | **before** any run row exists | **after** the run row exists |
1550
- | A `false` verdict leaves | no run at all — nothing appears in the dashboard | a run whose only entries are the evaluation jobs, rolling up to `success` |
1551
- | `sourceRepo` vs `workflowRepo` | two different repos | the same repo |
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.
1552
403
 
1553
- Two consequences of the same-repo shape are worth designing for. A workflow with ten jobs calls its filter ten times for one event each on its own agent with its own checkout and its own `$` so anything the predicate does happens that many times: keep it cheap and side-effect free. And if the predicate can answer differently for the same event, the workflow will _partially_ dispatch, running some jobs and not others.
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.
1554
405
 
1555
- **A held or rejected job is not filtered at all.** A job held for approval, or rejected by a context rule, already has a gate the hold or the rule so it never takes a filter verdict, and an approved job dispatches without one. Concretely: a path filter cannot stop an approval request for a job the change does not concern.
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/).
1556
407
 
1557
- ### Generating jobs per source repo
408
+ **Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context) simply adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic context resolving to an as-yet-unconfigured name does today.
1558
409
 
1559
- A global workflow's job generators run in the same pre-run evaluation as the filter, with both repos on disk. `sourceRepo` and `workflowRepo` are on the generator context, so one workflow repo can produce a different job set per source repo:
410
+ **Registration rejects a provably-unsatisfiable binding.** When a workflow is registered, KiCI statically checks every multi-context binding: a bound context that does not exist, a disabled one, or two contexts with mutually-exclusive fixed branch / trigger-type / repository restrictions (no value can satisfy both) makes the binding provably unsatisfiable, and the registration is rejected with a precise message naming the job, the contexts, and the rule for example `unsatisfiable context binding: job 'deploy' binds contexts [staging, my-testing] with mutually exclusive branch restrictions (no value satisfies all bound contexts)`. Bindings whose restrictions use globs are undecidable at registration and fall through to the dispatch-time gate check instead.
1560
411
 
1561
- ```ts
1562
- import { job, step, workflow, push, type DynamicJobFn } from '@kici-dev/sdk';
1563
- import { readFile } from 'node:fs/promises';
412
+ ### Job-level environment variables
1564
413
 
1565
- const perRepoJobs: DynamicJobFn = async ({ sourceRepo }) => {
1566
- if (!sourceRepo) return [];
1567
- const pkg = JSON.parse(await readFile(`${sourceRepo.path}/package.json`, 'utf8'));
1568
- return Object.keys(pkg.scripts ?? {})
1569
- .filter((s) => s.startsWith('ci:'))
1570
- .map((s) =>
1571
- job(s.replace(':', '-'), {
1572
- runsOn: ['kici:os:linux'],
1573
- steps: [step('run', async ({ $ }) => $`pnpm ${s}`)],
1574
- }),
1575
- );
1576
- };
414
+ The `env` property on a job provides static or dynamic environment variables:
1577
415
 
1578
- export default workflow('org-ci', {
1579
- on: [push({ repos: ['myorg/*'] })],
1580
- jobs: [perRepoJobs],
416
+ ```typescript
417
+ job('deploy', {
418
+ runsOn: 'default',
419
+ context: 'production',
420
+ env: { DEPLOY_TARGET: 'us-east-1' },
421
+ // Or dynamic:
422
+ // env: (event) => ({ DEPLOY_SHA: event.payload.after?.slice(0, 7) }),
423
+ steps: [
424
+ step('deploy', async (ctx) => {
425
+ // DEPLOY_TARGET is available in ctx.env
426
+ await ctx.$`deploy --region ${ctx.env.DEPLOY_TARGET}`;
427
+ }),
428
+ ],
1581
429
  });
1582
430
  ```
1583
431
 
1584
- The same `sourceRepo.path` caution applies: read the tree through it, and derive job names from the repo's _contents_, never from the path.
432
+ ### Concurrency groups
1585
433
 
1586
- ## Enabling global workflows
434
+ Jobs can define their own concurrency groups to control concurrent execution within a context. For workflow-level concurrency (which applies to all jobs in a workflow), see [Concurrency groups](https://docs.kici.dev/user/concurrency/).
1587
435
 
1588
- Global workflows are gated by a **fleet-wide master switch** held by the orchestrator operator, off by default. Until it is on, `repos:`-bearing workflows are registered but never dispatched.
436
+ Control concurrent deployments to the same context:
1589
437
 
1590
- 1. **The operator enables it cluster-wide** with `kici-admin cluster-settings set --global-workflows-enabled true`. This is the kill-switch — every per-org control below is ignored while it is off, and it cannot be flipped from the dashboard. The dashboard's **Settings → Global workflows** tab shows its current state as a read-only badge.
1591
- 2. In the dashboard → **Settings → Global workflows**, decide which authoring/source controls you need. These per-org lists stay dashboard-editable; an org that has set none means "no per-org restrictions", not a denial.
438
+ ```typescript
439
+ job('deploy', {
440
+ runsOn: 'default',
441
+ context: 'production',
442
+ concurrencyGroup: 'production-api',
443
+ // Or dynamic:
444
+ // concurrencyGroup: (event) => `review-${event.payload.pull_request.number}`,
445
+ steps: [/* ... */],
446
+ });
447
+ ```
1592
448
 
1593
- | Setting | What it controls | Typical use |
1594
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
1595
- | 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. |
1596
- | 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-*`. |
1597
- | Elevated access | **Deprecated and not enforced.** Stored and echoed back, but nothing reads it — a global workflow's job receives no secrets, so there is no access for it to grant. See _Secrets are not available_. | None. Clear the list so it does not imply a grant that is not in force. |
449
+ If no `concurrencyGroup` is specified, the context name is used as the default concurrency group. For a job bound to multiple contexts, the default is the **first** bound context's name.
1598
450
 
1599
- 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-*`).
451
+ ### Step context
1600
452
 
1601
- Patterns match repo identifiers by the same rule as `repos:` on a trigger: an identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own and a wildcard segment matches one. `myorg/*` covers `myorg/.github`, and `**` covers every repo in the org. Review any existing entry that relies on a wildcard to reach — or to spare — a dot-prefixed repo name.
453
+ Inside a step, the `ctx` object provides:
1602
454
 
1603
- ### Saving and reverting
455
+ | Property | Type | Description |
456
+ | ------------- | ------------------------------------- | ----------------------------------------------------------------------------------------- |
457
+ | `ctx.context` | `string \| undefined` | Resolved context name (undefined for jobs without context) |
458
+ | `ctx.env` | `Record<string, string \| undefined>` | Environment variables (merged from system, org, source, and job-level `env`) |
459
+ | `ctx.secrets` | `StepSecretsTyped` | Async accessor for bound secrets (get, expose, has, getMeta, list, mountFile, exposeFile) |
1604
460
 
1605
- 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.
461
+ | Method | Returns | Description |
462
+ | -------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
463
+ | `await ctx.secrets.get(key)` | `string` | Retrieve a secret value. Throws `SecretNotFoundError` if not found. |
464
+ | `await ctx.secrets.expose(key)` | `void` | Set the secret as an environment variable for this step — visible via `ctx.env` and to child processes (`process.env`). Throws `SecretNotFoundError` if not found. |
465
+ | `ctx.secrets.has(key)` | `boolean` | Check if a secret key exists. Synchronous, never throws. |
466
+ | `ctx.secrets.getMeta(key)` | `SecretMeta \| undefined` | Retrieve metadata (value, backend name, scope path) for a resolved secret. Returns `undefined` if not found. |
467
+ | `ctx.secrets.list()` | `string[]` | Every secret key available to the step, sorted alphabetically. Synchronous, never throws. |
468
+ | `await ctx.secrets.mountFile(opts)` | `{ path }` | Materialise one or more secrets to a per-step tmpfile (auto-removed at step end). See [Secrets → Mounting secrets as files](https://docs.kici.dev/user/secrets/#mounting-secrets-as-files). |
469
+ | `await ctx.secrets.exposeFile(envVar, opts)` | `{ path }` | `mountFile` plus `process.env[envVar] = path`; the env var is unset at step end. |
470
+ | `ctx.setSecretOutput(key, val)` | `void` | Publish an encrypted secret output from this job, consumable by downstream jobs via `needs`. Never logged or stored in plaintext. |
1606
471
 
1607
- ## Security model
472
+ The full secrets API — including `SecretFileOptions`, log masking, and the canonical `sops` example — is documented in [Secrets](https://docs.kici.dev/user/secrets/).
1608
473
 
1609
- ### Two independent axes
474
+ ## Environment variable merge precedence
1610
475
 
1611
- A global workflow fires only if:
476
+ When a job targets a context, variables are merged in this order (last wins):
1612
477
 
1613
- 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:
1614
- - At registration time (extraction from the lock file — non-matching globals are dropped, and the orchestrator logs `Global workflows excluded from registration` naming each one).
1615
- - At dispatch time (defense-in-depth — policy changes after registration still take effect).
1616
- 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.
478
+ 1. **Allowed system vars** -- `PATH`, `HOME`, etc. from the agent process
479
+ 2. **Sandbox defaults** -- `FORCE_COLOR=1`
480
+ 3. **KICI\_\* system vars** -- orchestrator-generated metadata
481
+ 4. **Org-level context vars** -- from the dashboard, managed per-context
482
+ 5. **Source-level overrides** -- per-repository overrides (skips locked vars)
483
+ 6. **Job env** -- from the `env` property in the SDK
484
+ 7. **`setEnv()` calls** -- runtime modifications within steps
1617
485
 
1618
- Both checks are logged to the orchestrator. Grep for `Global workflows excluded from registration` (registration time) and `Skipping global workflow dispatch` (dispatch time) to see enforcement in action.
486
+ > **Note:** Secrets are NOT part of the environment variable merge. They are delivered to the step context via IPC and accessed through `ctx.secrets`, not through `process.env`. See the [step context](https://docs.kici.dev/user/contexts/#step-context) section above.
1619
487
 
1620
- Both checks read the settings of the organization the **event's source** resolves to. If no webhook source maps the event's routing key to an organization, the orchestrator resolves the built-in `__default__` organization anchor instead — and since nobody has enabled global workflows for that anchor, every global workflow is refused. The registration log line carries the organization it decided against plus the remedy, so this case is distinguishable from a real opt-in that is simply switched off. See the troubleshooting table below.
488
+ ## Protection rules
1621
489
 
1622
- ### Secrets are not available
490
+ Contexts can have protection rules that gate job execution:
1623
491
 
1624
- A global workflow's job is dispatched with **no secret material** — not the source repo's, and not the workflow repo's own. The organization-wide dispatch path binds no secret contexts, so a `contexts:` declaration on a global workflow resolves to nothing and any secret the steps expect is simply absent. Plan for it: a global workflow is for checks, policy and reporting that need only the two checkouts, not for deploys that need credentials.
492
+ ### Branch restrictions
1625
493
 
1626
- This is about your **stored secrets**, not about repository access: the job is still handed a short-lived clone token for each repo it checks out, which is how the dual checkout works at all. What it does not get is anything from a secret context.
494
+ Limit which branches can deploy to a context:
1627
495
 
1628
- To run something that needs secrets on a source repo's event, put those jobs in a per-repository workflow in that repo, where the workflow's `contexts:` resolve normally.
496
+ ```
497
+ Allowed branches: main, release/*
498
+ ```
1629
499
 
1630
- The **Elevated access** setting reads as the way to lift this, and it is not: it is **deprecated and never consulted**. Nothing in the dispatch path reads the list, and adding a repo to it does not make any secret readable. It is kept only so an existing value stays visible and clearable, and is removed at the next major version — see [Deprecations](https://docs.kici.dev/user/deprecations/).
500
+ Jobs from other branches are rejected immediately with an error message.
1631
501
 
1632
- ## When does it fire?
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:
1633
503
 
1634
- 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.
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`.
1635
506
 
1636
- 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:`.
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.
1637
508
 
1638
- A global workflow that declares a `filter` or a job generator is decided by one **evaluation job per (event × workflow repo)**, dispatched before any run exists. That job checks out both repos once and evaluates every candidate workflow from that repo, so ten global workflows in one CI repo cost one evaluation, not ten.
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.
1639
510
 
1640
- When that evaluation cannot reach a verdict it fails, breaches its budget, or never reports — the workflows it was deciding on **do not run**. On a provider that supports commit checks, that posts a `failure` check named **`KiCI: Organization workflow evaluation`** on the source commit, so the outcome is visible instead of silent. Three things to know about it:
511
+ A run with no single source branch presents no branch, and a branch restriction then rejects it:
1641
512
 
1642
- - The check is posted whether the evaluation failed **outright** or only **partly**. A per-workflow budget breach, or a `filter` that throws, leaves that one workflow undecided while its neighbours from the same repo are decided and run normally; the check then names only the undecided ones. So a broken `filter` is reported the same way whether or not other global workflows happen to share its repo.
1643
- - Branch protection that lists required checks by name is unaffected, because the check is not on that list. Merge automation that requires _every_ check to be green will block on it.
1644
- - **The check clears only on a new commit.** A provider redelivery of the same event is dropped as a duplicate, so re-delivering the webhook will not re-run the evaluation.
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.
1645
517
 
1646
- ## Approval gates are not supported
518
+ The rejection reason says so:
1647
519
 
1648
- A global workflow cannot carry an `approval` gate, at the workflow level or on a job. Approval holds are applied by the per-repository dispatch path; the global path dispatches its jobs without consulting one, so a gate declared here would never be enforced. `kici compile` refuses it with `error [E124]` rather than accepting a security control the workflow does not actually have. A job produced by a `dynamicJob` generator never passes through the compiler, so that case is caught at dispatch instead — the orchestrator logs an error naming the workflow and job, and runs it ungated.
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
+ ```
1649
527
 
1650
- To gate a deployment behind a human, put the gated jobs in a workflow whose triggers carry no `repos:`.
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.
1651
529
 
1652
- ## Re-running is not supported
530
+ ### Required reviewers
1653
531
 
1654
- A global workflow's run cannot be re-run. The rerun path resolves a workflow out of the repo the run acted on, and for a global workflow that is the **source** repo — not the workflow repo that declares it. Re-running is refused with an error naming both repos rather than resolving the wrong one, which for a source repo carrying a same-named workflow would silently run that workflow instead, with the source repo's credentials and none of the global job configuration.
532
+ Require manual approval before a job can proceed:
1655
533
 
1656
- To run it again, push a new commit to the source repo (a provider redelivery of the same event is dropped as a duplicate), or trigger it from the workflow repo.
534
+ ```
535
+ Required reviewers: alice, bob
536
+ ```
1657
537
 
1658
- ## Requirements a filter places on the run
538
+ When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-approve) command, or the API. Held runs expire after a configurable timeout.
1659
539
 
1660
- A `filter` reads the source tree, so the evaluation must be able to obtain one. A job that restores its workflow source from the cache and has no source repository to clone from fails with an explicit error rather than evaluating the filter against an empty tree. This applies to dispatch paths that run without a source repository configured — a filter and such a path are mutually exclusive; drop one or the other.
540
+ This operator-set rule is the **mandatory** form of an approval gate. Workflow authors can also declare gates in code with `approval` at step, job, or workflow level see [Approval gates](https://docs.kici.dev/user/approvals/). Both forms use the same held-element mechanism and the same queue.
1661
541
 
1662
- ## Troubleshooting
542
+ ### Wait timer
1663
543
 
1664
- | Symptom | Likely cause | Where to look |
1665
- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1666
- | Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` (dispatch time) / `Global workflows excluded from registration` (registration time) |
1667
- | A global workflow is never registered at all — it is absent from `kici-admin registration list` | The push that should have registered it resolved to the `__default__` organization anchor, because no webhook source maps its routing key to an organization, or the fleet-wide master switch is off. | Orchestrator log: `Global workflows excluded from registration` with `"orgId": "__default__"`. Its `remedy` field names both fixes — map the source (`kici-admin source update <routingKey> --customer-id <org>`) and enable global workflows cluster-wide if it is not already (`kici-admin cluster-settings set --global-workflows-enabled true`). |
1668
- | `repos:` has no effect — workflow only fires on its own repo | The fleet-wide master switch is off. Without it, the orchestrator treats the workflow as per-repo-only. | Check the fleet-wide switch with `kici-admin cluster-settings show`. The dashboard → Settings → Global workflows tab shows it as a read-only badge. |
1669
- | Secrets unavailable in a global job | Expected — a global workflow's job receives no secrets at all, and the _Elevated access_ list is not enforced. | Move the jobs that need credentials into a per-repository workflow in the repo that owns the secrets |
1670
- | 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. |
1671
- | Global workflow registered, enabled, allowed — and still no run appears | Its `filter` returned `false`. A global filter runs before the run is created, so a suppressed workflow leaves nothing behind at all. | [Reading a global workflow's filter output](https://docs.kici.dev/user/global-workflows/#reading-a-global-workflows-filter-output) — the evaluation round's own log. The orchestrator also logs `Global workflow skipped by eval round`, naming the workflow and the reason. |
1672
- | Global workflow never fires for one particular source repo | Its `repos:` patterns do not match that repo's identifier. | Orchestrator log: `Global workflows dropped by their repos filter` — one line per delivery, naming each dropped workflow, its repo and its patterns. |
1673
- | A `failure` check named `KiCI: Organization workflow evaluation` on a commit | The pre-run evaluation failed or timed out, so the global workflows from that repo were not run. | Orchestrator log for the evaluation job; push a new commit to re-evaluate (a redelivery is dropped as a duplicate). |
1674
- | Same-repo workflow shows a `success` run with no jobs in it | Its `filter` returned `false`. A same-repo filter runs after the run exists, so the run remains, carrying only the evaluation jobs. | The run detail page — the evaluation job's log records the filter verdict. |
1675
- | Re-run is refused with "Cannot re-run an organization-wide workflow" | Expected — the run executed against a source repo that does not declare the workflow. | [Re-running is not supported](https://docs.kici.dev/user/global-workflows/#re-running-is-not-supported) — push a new commit to the source repo instead. |
1676
- | Every global workflow stopped running right after an orchestrator upgrade | The agents were not upgraded first. An agent older than v0.5.0 cannot evaluate a global workflow, and one containing a `dynamicJob` now needs an evaluation even without a `filter` — so its **static** jobs stop too. | The `KiCI: Organization workflow evaluation` check names the agent versions it found. Upgrade every `kici:role:init-runner` agent to v0.5.0 or newer. |
544
+ Add a mandatory delay before deployment starts:
1677
545
 
1678
- ### Reading a global run in the dashboard
546
+ ```
547
+ Wait timer: 300 seconds
548
+ ```
1679
549
 
1680
- A global run is attributed to the **source** repo the repo whose event
1681
- triggered it, and whose code the jobs check out. Its run detail page names both
1682
- repos, so you can tell it apart from an ordinary per-repo run:
550
+ The job waits for the specified duration before proceeding. Useful for staged rollouts.
1683
551
 
1684
- | Row | Shows |
1685
- | ------------ | ------------------------------------------------------------------------ |
1686
- | `Repository` | the source repo — the one the run acted on |
1687
- | `Defined in` | the workflow repo, tagged `Organization-wide`. Absent on an ordinary run |
1688
- | `Workflow` | links into the **workflow** repo, on its default branch |
552
+ ### Minimum trust
1689
553
 
1690
- The `Workflow` link points at the workflow repo's default branch rather than at
1691
- a commit: the run's own commit belongs to the source repo, and nothing records
1692
- which commit of the workflow repo a given run used. So the link always shows the
1693
- file as it stands now, which may have changed since the run.
554
+ Hold a job whose run came from a fork:
1694
555
 
1695
- The `Payload` tab shows the source repo's event — the webhook delivery the
1696
- workflow reacted to, which for a global workflow comes from a repo you may not
1697
- own. A global run dispatched before your orchestrator stored payloads for this
1698
- path has none, and its tab reports that it could not load one.
556
+ ```
557
+ Minimum trust: trusted
558
+ ```
1699
559
 
1700
- #### Who can see it
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 |
1701
565
 
1702
- A global run belongs to **both** repos, so a member whose role is scoped to
1703
- either one reaches it — the team whose push triggered it, and the team that
1704
- authored the workflow. Both see it in the run list, in the repository filter
1705
- (which offers both names), and on the run detail page. Cancelling follows the
1706
- same rule, so the team whose workflow is running can always stop it.
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.
1707
567
 
1708
- Releasing a **held** run is the one exception: approving a hold permits code to
1709
- run against the source repo, so it stays with a member scoped to that repo. A
1710
- member scoped only to the workflow repo sees the run but not its hold.
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.
1711
569
 
1712
- This applies only where the two repos genuinely differ. An ordinary per-repo run
1713
- records no separate workflow repo and is scoped to its own repo exactly as
1714
- before, and a member scoped to neither repo sees nothing in either case.
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.
1715
571
 
1716
- ### Reading a global workflow's filter output
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.
1717
573
 
1718
- A global workflow's `filter` runs in a pre-run evaluation round, and that round
1719
- decides whether a run exists at all — so on the path where it suppresses a
1720
- workflow there is no run, and nothing appears in the dashboard. The round's own
1721
- log is still recorded. Read it with the orchestrator admin CLI, in two steps:
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).
1722
575
 
1723
- ```bash
1724
- # 1. Find the round. Its workflow name is __globaleval__<owner>/<repo> of the
1725
- # WORKFLOW repo. In the JSON rows, `id` is the job id and `run_id` is the
1726
- # run id.
1727
- kici-admin queue list --workflow-name '__globaleval__myorg/ci-pipelines' --limit 5 --json
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).
1728
577
 
1729
- # 2. Print the round's log (step 0 is the evaluation itself).
1730
- kici-admin runs logs <run_id> --job <id>
1731
- ```
578
+ See the [CI security architecture docs](https://docs.kici.dev/architecture/security/ci-security/) for the full trust resolution flow.
1732
579
 
1733
- Use `--json` on the first command: the plain table abbreviates both ids to their
1734
- first eight characters, and the second command needs them in full.
580
+ ### Security approval queue
1735
581
 
1736
- The two steps need different permissions, so run both with an **owner or admin**
1737
- token. Step 1 reads the dispatch queue, which requires `secret.read` — an auditor
1738
- token is refused with a 403 and never reaches step 2. Step 2 requires only
1739
- `run.read`, which every role carries.
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.
1740
583
 
1741
- Anything your `filter` writes with `console.log` appears there, alongside the
1742
- per-candidate verdicts the round recorded.
584
+ Held runs can be approved:
1743
585
 
1744
- ## See also
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
587
+ - Via a PR comment: `/kici approve` (commenter must have `ci_trust:write+`)
1745
588
 
1746
- - [Architecture global workflows](https://docs.kici.dev/architecture/global-workflows/) dual-query dispatch flow, cross-provider auth, security model, lock-file schema.
1747
- - [Universal-git provider](https://docs.kici.dev/user/providers/universal-git/#global-workflows) — how global workflows interact with `generic:<orgId>:<sourceId>` routing keys.
1748
- - [SDK reference](https://docs.kici.dev/user/sdk-reference/) — the full set of triggers that accept `repos:`.
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.
1749
590
 
1750
- ---
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.
1751
592
 
1752
- ## Idempotent steps and check mode
593
+ ### Concurrency limits
1753
594
 
1754
- Source: https://docs.kici.dev/user/idempotent-steps/
595
+ Control how many jobs can run simultaneously in a context:
1755
596
 
1756
- An **idempotent step** describes _desired state_ rather than a fixed sequence of
1757
- commands. You give the step a `check` function that inspects the world and a
1758
- `run` function that converges it. KiCI then executes the workflow in one of two
1759
- modes:
597
+ ```
598
+ Concurrency limit: 1
599
+ Strategy: queue (or cancel-pending)
600
+ ```
1760
601
 
1761
- - **Apply mode** (the default): for each step, `check()` runs first; on drift the
1762
- step applies the change; when already in sync the step is skipped.
1763
- - **Check mode** (`--check`): for each step, `check()` runs and KiCI reports what
1764
- _would_ change — **without changing anything**. This is the same model as a
1765
- dry-run plan: you see the drift before any side effect happens.
602
+ The concurrency limit is a positive integer; leave it unset for unlimited concurrency.
1766
603
 
1767
- This turns a workflow into convergent configuration management: re-running an
1768
- apply is safe (in-sync steps do nothing), and a check-mode run is a read-only
1769
- preview you can gate a build on.
604
+ - **queue** -- new jobs wait in a FIFO queue (with configurable timeout, default 1 hour)
605
+ - **cancel-pending** -- pending (queued) jobs are cancelled when the limit is reached
1770
606
 
1771
- ## Authoring a checked step
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.
1772
610
 
1773
- Add a `check` facet to the existing `step()` factory. When `check` is present,
1774
- `run` becomes the _apply_ function and receives the drift value `check`
1775
- returned:
611
+ ## Dashboard management
1776
612
 
1777
- ```typescript
1778
- import { step, z } from '@kici-dev/sdk';
613
+ ### Creating contexts
1779
614
 
1780
- const configureNginx = step('configure-nginx', {
1781
- // optional schema for the drift value — gives the dashboard a typed shape
1782
- drift: z.object({ want: z.string() }),
615
+ Navigate to **Settings > Contexts** in the dashboard. Click **New context** to choose the context name and type (Fixed or Glob).
1783
616
 
1784
- // read-only inspection; return null when already in the desired state
1785
- check: async (ctx) => {
1786
- const current = await ctx.$`nginx -T`;
1787
- return current.stdout.includes(DESIRED) ? null : { want: DESIRED };
1788
- },
617
+ - **Fixed** -- applies to jobs that declare exactly this context name, like `staging` or `production`
618
+ - **Glob** -- applies to any context name a job declares that matches the pattern, e.g. `review/*` matches a job with `context: 'review/PR-123'`
1789
619
 
1790
- // human-readable preview line REQUIRED when check is set. It is the drift's
1791
- // serializable face: it streams to the logs and persists for the dashboard.
1792
- summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
620
+ The contexts list shows each context's type, whether test runs may use it (the `allowLocalExecution` flag -- see the [testing guide](https://docs.kici.dev/user/testing-guide/)), and whether it is enabled.
1793
621
 
1794
- // apply runs only when check returned drift (apply mode); receives that drift
1795
- run: async (ctx, drift) => {
1796
- await writeConfig(drift.want);
1797
- return { reloaded: true };
1798
- },
622
+ ### Context detail page
1799
623
 
1800
- // optional runs when check returned null, to produce the step's outputs
1801
- whenInSync: async () => ({ reloaded: false }),
1802
- });
1803
- ```
624
+ Each context has four tabs:
1804
625
 
1805
- ### The facet fields
626
+ 1. **Variables** -- manage key-value pairs with lock toggles. Locked variables cannot be overridden by source-level overrides. Source overrides are managed in a sub-tab.
1806
627
 
1807
- | Field | Required | Purpose |
1808
- | ------------ | ---------------- | ---------------------------------------------------------------------- |
1809
- | `check` | to opt in | Read-only inspection. Return a drift value, or `null` when in sync. |
1810
- | `summarize` | when `check` set | Human-readable, serializable preview of the drift. Streams + persists. |
1811
- | `run` | always | Apply function. With `check`, it receives the drift as its second arg. |
1812
- | `whenInSync` | optional | Produces the step's outputs when `check` returned `null`. |
1813
- | `drift` | optional | Schema that validates / shapes the drift value. |
628
+ 2. **Secrets** -- view bound secret scopes and their resolved secret count. Add bindings by specifying scope glob patterns (e.g., `aws/prod/**`).
1814
629
 
1815
- `summarize` is **required** whenever `check` is declared. `run` and `whenInSync`
1816
- both produce the same output type — one output shape per step, whichever path
1817
- runs. Every other step facet (`cache`, `rules`, `continueOnError`, `timeout`,
1818
- `retry`, `approval`, `onCancel`, `cleanup`, `outputs`) composes unchanged.
630
+ 3. **Protection** -- configure branch restrictions, required reviewers, wait timers, and concurrency limits with enable toggles for each section. Turning a section's toggle off and saving clears that rule on the context, so the gate stops applying to new runs. Emptying the hold expiry field clears it too, and held runs fall back to the default one-hour hold window.
1819
631
 
1820
- A plain `step()` without `check` keeps its exact current behavior — the check
1821
- facet is fully optional.
632
+ 4. **History** -- view filtered runs targeting this context.
1822
633
 
1823
- ## Run modes
634
+ ### Bound contexts on runs
1824
635
 
1825
- A run carries one of three modes:
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.
1826
637
 
1827
- | Mode | CLI flags | Behavior |
1828
- | --------------------- | ------------------------- | ------------------------------------------------------------------------------------------ |
1829
- | `apply` | (default, no flags) | Converge: drift ⇒ apply ⇒ **applied**; null ⇒ **in sync** (skipped). |
1830
- | `check` | `--check` | Preview only: drift ⇒ **would change**; null ⇒ **in sync**. Never applies. Always exits 0. |
1831
- | `check-fail-on-drift` | `--check --fail-on-drift` | Same as check, but the run **fails** if any step reports drift. |
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.
1832
639
 
1833
- Per-step outcomes:
640
+ ### Secrets management
1834
641
 
1835
- - **applied** drift was found and the step applied the change (apply mode).
1836
- - **in sync** — `check` returned `null`; nothing to do.
1837
- - **would change** — drift was found in check mode; the change was previewed, not applied.
1838
- - **no check** — a plain step (no `check`) reached under check mode. A
1839
- side-effecting step can't be safely previewed, so it is skipped.
642
+ Secrets are individual encrypted values organized by scope paths (e.g., `aws/prod`, `databases/postgres`). Scopes are bound to contexts via bindings:
1840
643
 
1841
- In check mode KiCI never invokes a checked step's `run` (apply) the preview is
1842
- guaranteed side-effect-free.
644
+ - **Scope-centric view** (Secrets page): tree view of scopes with per-scope context binding checkboxes
645
+ - **Context-centric view** (inside context detail): bound scopes, resolved secrets, add binding
1843
646
 
1844
- ## Running in check mode
647
+ When scope paths collide on the same key name, the longer (more specific) path wins.
1845
648
 
1846
- `--check` and `--fail-on-drift` control drift reporting on `kici run remote`:
649
+ ## Type generation
1847
650
 
1848
- ```bash
1849
- # Apply (default): converge the workflow.
1850
- kici run push --local
1851
- kici run remote my-fixture
651
+ Running `kici types` generates two augmented interfaces: `KnownSecretKeys` (union of all secret keys across all contexts) and `ContextSecrets` (per-context key unions):
1852
652
 
1853
- # Check: report drift, change nothing. Always exits 0.
1854
- kici run remote my-fixture --check
653
+ ```typescript
654
+ interface KnownSecretKeys {
655
+ DB_PASSWORD: string;
656
+ API_KEY: string;
657
+ }
1855
658
 
1856
- # Check + fail on drift: fail the run when any step reports drift. Use this as a
1857
- # CI gate ("fail the build if prod has drifted").
1858
- kici run remote my-fixture --check --fail-on-drift
659
+ interface ContextSecrets {
660
+ production: 'DB_PASSWORD' | 'API_KEY';
661
+ staging: 'DB_PASSWORD';
662
+ }
1859
663
  ```
1860
664
 
1861
- `--fail-on-drift` only modifies check mode passing it without `--check` is an
1862
- error.
665
+ `KnownSecretKeys` narrows `ctx.secrets.get()` and `ctx.secrets.expose()` key parameters to valid key names. `ContextSecrets` maps each context to its available secret key names as a string union. Dynamic contexts fall back to the full `KnownSecretKeys` union.
1863
666
 
1864
- ## Where outcomes show up
667
+ ---
1865
668
 
1866
- A check-mode run is labeled in the dashboard with a **CHECK MODE — preview**
1867
- badge on the run header. Each step shows its outcome chip — applied / in sync /
1868
- would change / no check — and, when drift was detected, the `summarize` line
1869
- describing what would change. The rendering is read-only.
669
+ ## Dashboard
1870
670
 
1871
- ## See also
671
+ Source: https://docs.kici.dev/user/dashboard/
1872
672
 
1873
- - [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.
1874
- - [Core SDK reference](https://docs.kici.dev/user/sdk/core/) — the `step()`, `job()`, and `workflow()` factories the check facet extends.
1875
- - [Lock file and drift](https://docs.kici.dev/user/lock-file-and-drift/) — how the lock file carries step capability flags.
673
+ The KiCI dashboard is the browser interface for monitoring workflow runs, inspecting jobs and logs, and managing your organization. It signs in via OIDC and talks to KiCI over its API.
674
+
675
+ This guide is split across the following pages:
676
+
677
+ | Page | Covers |
678
+ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
679
+ | [Getting started](https://docs.kici.dev/user/dashboard/getting-started/) | Onboarding checklist and your organizations list |
680
+ | [Navigation and layout](https://docs.kici.dev/user/dashboard/navigation/) | Sidebar, mobile nav, theme, time display, shortcuts, error pages |
681
+ | [Runs and logs](https://docs.kici.dev/user/dashboard/runs/) | Run list, run detail, the log viewer |
682
+ | [Settings](https://docs.kici.dev/user/dashboard/settings/) | Members, roles, teams, keys, sources, billing, security, support access |
683
+ | [Workflows, diagnostics, and orchestrators](https://docs.kici.dev/user/dashboard/workflows-and-diagnostics/) | Registered workflows, infra health, per-cluster views |
684
+ | [Contexts, secrets, and approvals](https://docs.kici.dev/user/dashboard/contexts-and-secrets/) | Contexts, secret scopes, approval queue |
685
+ | [Activity and DLQ](https://docs.kici.dev/user/dashboard/activity-and-dlq/) | Forensic activity log and dead-letter queue |
686
+ | [Notifications](https://docs.kici.dev/user/dashboard/notifications/) | Personal run notifications and org-wide channels, subscriptions, delivery log |
687
+ | [Account](https://docs.kici.dev/user/dashboard/account/) | Personal account settings |
1876
688
 
1877
689
  ---
1878
690
 
@@ -2102,7 +914,7 @@ registries: [
2102
914
  ## Security model
2103
915
 
2104
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.
2105
- - **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.
2106
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.
2107
919
  - **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
2108
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.
@@ -2294,11 +1106,12 @@ commonly, an offline `{ issuer, jwks }` file for air-gapped checks.
2294
1106
 
2295
1107
  ### Why you supply it out-of-band
2296
1108
 
2297
- Given there's a single issuer, why pass it at all instead of letting the
2298
- verifier read it from the token? Because the issuer named **inside** a token
2299
- cannot be trusted: a forged bundle could carry a token that names
2300
- `iss: https://attacker.example` _and_ bundle a key set that "verifies" it,
2301
- 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
2302
1115
  therefore pins to an issuer you supply out-of-band and checks the token against
2303
1116
  _that_ — the bundle is verified against a key set you trust, not one it shipped
2304
1117
  with. Naming the trust root is a security requirement, not a multiple-choice
@@ -2740,7 +1553,9 @@ Note that `get()` is async -- you must `await` the result.
2740
1553
 
2741
1554
  ## Typed secrets
2742
1555
 
2743
- 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.
2744
1559
 
2745
1560
  See [CLI reference](https://docs.kici.dev/user/cli/authoring-and-local/#kici-types) for the `kici types` command.
2746
1561