@kici-dev/compiler 0.1.22 → 0.1.23

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 (39) hide show
  1. package/dist/cli.js +20 -6
  2. package/dist/commands/compile.d.ts +6 -0
  3. package/dist/commands/compile.js +6 -3
  4. package/dist/commands/docs.d.ts +8 -8
  5. package/dist/commands/docs.js +35 -16
  6. package/dist/commands/org.js +2 -2
  7. package/dist/commands/run.d.ts +16 -1
  8. package/dist/commands/run.js +86 -14
  9. package/dist/commands/test.d.ts +4 -0
  10. package/dist/commands/types.d.ts +2 -0
  11. package/dist/commands/types.js +1 -1
  12. package/dist/fixtures/describe-event.d.ts +6 -0
  13. package/dist/fixtures/describe-event.js +18 -0
  14. package/dist/fixtures/picker.d.ts +19 -0
  15. package/dist/fixtures/picker.js +64 -0
  16. package/dist/llm-context/llms-architecture.txt +1440 -0
  17. package/dist/llm-context/llms-cli.txt +2386 -0
  18. package/dist/llm-context/llms-features.txt +2389 -0
  19. package/dist/llm-context/llms-full.txt +976 -317
  20. package/dist/llm-context/llms-getting-started.txt +519 -0
  21. package/dist/llm-context/llms-patterns.txt +1324 -0
  22. package/dist/llm-context/llms-providers.txt +805 -0
  23. package/dist/llm-context/llms-sdk.txt +3725 -0
  24. package/dist/llm-context/llms.txt +13 -0
  25. package/dist/local-executor/index.js +40 -3
  26. package/dist/local-executor/job-runner.d.ts +2 -0
  27. package/dist/local-executor/job-runner.js +36 -4
  28. package/dist/local-executor/types.d.ts +2 -0
  29. package/dist/lockfile/generator.js +13 -4
  30. package/dist/remote/platform-client.d.ts +6 -0
  31. package/dist/remote/uploader.js +1 -0
  32. package/dist/templates/package-json.js +1 -1
  33. package/dist/test-runner/rule-evaluator.d.ts +1 -1
  34. package/dist/test-runner/rule-evaluator.js +2 -1
  35. package/dist/test-runner/step-context.d.ts +1 -1
  36. package/dist/test-runner/step-context.js +7 -2
  37. package/dist/types.d.ts +6 -2
  38. package/package.json +4 -4
  39. package/sbom.spdx.json +35 -35
@@ -0,0 +1,2386 @@
1
+ # KiCI CLI and authoring
2
+
3
+ This bundle covers: Running the CLI: compile, test, run local/remote, auth, hooks, lock-file drift.
4
+
5
+ ## CLI authentication
6
+
7
+ Source: https://docs.kici.dev/user/cli-auth/
8
+
9
+ The KiCI CLI supports three authentication methods: browser-based OAuth (default), device authorization flow (for headless environments), and API key paste (for CI/CD pipelines).
10
+
11
+ ## Authentication methods
12
+
13
+ ### Browser OAuth (default)
14
+
15
+ The default `kici login` flow:
16
+
17
+ 1. Opens your default browser to the KiCI identity provider
18
+ 2. You authenticate in the browser
19
+ 3. The CLI receives a token via localhost callback
20
+ 4. A personal access token (PAT) is created and stored locally
21
+
22
+ ```bash
23
+ kici login
24
+ ```
25
+
26
+ The CLI auto-detects headless environments (SSH sessions, CI runners) and switches to device flow automatically.
27
+
28
+ ### Device flow (headless)
29
+
30
+ For environments without a browser (SSH, remote servers):
31
+
32
+ ```bash
33
+ kici login --device
34
+ ```
35
+
36
+ This displays a URL and a code. Open the URL on any device, enter the code, and authenticate. The CLI polls for completion.
37
+
38
+ ### API key paste
39
+
40
+ For CI/CD pipelines and automated environments, paste an API key directly:
41
+
42
+ ```bash
43
+ kici login --token kici_sk_abc123...
44
+ ```
45
+
46
+ The API key (starts with `kici_sk_`) is passed directly as the flag value and stored in your local config file.
47
+
48
+ ## kici logout
49
+
50
+ Revoke your PAT and clear local authentication:
51
+
52
+ ```bash
53
+ kici logout
54
+ ```
55
+
56
+ This:
57
+
58
+ 1. Revokes the PAT on the server (preventing further use)
59
+ 2. Clears auth fields from the local config file
60
+ 3. Preserves non-auth settings (active org, default clusters, Platform endpoint)
61
+
62
+ ## Organization management
63
+
64
+ ### List organizations
65
+
66
+ ```bash
67
+ kici org list
68
+ ```
69
+
70
+ Shows all organizations you belong to, with your role in each. The active organization is marked with an asterisk.
71
+
72
+ ### Switch active organization
73
+
74
+ ```bash
75
+ kici org use <name-or-id>
76
+ ```
77
+
78
+ Name matching is case-insensitive. You can also use the organization ID directly.
79
+
80
+ The active organization is both the scope for org-scoped commands (`kici runs list`, `kici diagnostics`, `kici secrets list`, …) **and** the default target for `kici run remote`. After `kici login` and `kici org use <org>`, `kici run remote` dispatches to that org through the Platform — that is the complete path to a remote run. Override the target for a single run with `kici run remote --org <id>`.
81
+
82
+ If an organization has more than one connected orchestrator cluster, set its default cluster once with `kici orchestrators use <name>` (list them with `kici orchestrators list`). `kici run remote` then targets that cluster unless you pass `--orchestrator <name>`. With a single connected orchestrator the cluster is selected automatically.
83
+
84
+ ### Show current organization
85
+
86
+ ```bash
87
+ kici org current
88
+ ```
89
+
90
+ Displays the currently active organization name and ID.
91
+
92
+ ## Auth status
93
+
94
+ `kici org current` shows your current login state and active organization:
95
+
96
+ ```bash
97
+ kici org current
98
+ ```
99
+
100
+ It reports whether you are logged in and which organization is active. PAT
101
+ expiry and the full list of your tokens are managed from the dashboard (see
102
+ "Dashboard management" below).
103
+
104
+ ## Personal access tokens
105
+
106
+ Personal access tokens (PATs) are created automatically when you log in via OAuth. You can also create and manage PATs through the dashboard.
107
+
108
+ ### How PATs work
109
+
110
+ - **User-scoped**: PATs work across all organizations you belong to
111
+ - **120-day default expiry**: Configurable when creating from the dashboard
112
+ - **Named per machine**: Each login creates a PAT named after the machine hostname
113
+ - **Permission inheritance**: PATs inherit your effective role permissions in each org
114
+
115
+ ### PATs vs API keys
116
+
117
+ | | Personal access tokens | API keys |
118
+ | ---------- | ---------------------- | --------------- |
119
+ | Scope | User (cross-org) | Organization |
120
+ | Prefix | `kici_pat_` | `kici_sk_` |
121
+ | Created by | CLI login or dashboard | Dashboard |
122
+ | Expiry | 120 days (default) | No expiry |
123
+ | Use case | Developer CLI access | CI/CD pipelines |
124
+
125
+ ### Dashboard management
126
+
127
+ Create, view, and revoke PATs from the dashboard:
128
+
129
+ 1. Click your avatar in the sidebar
130
+ 2. Select **Account settings**
131
+ 3. Navigate to the **Personal access tokens** tab
132
+
133
+ From here you can:
134
+
135
+ - Create PATs with custom names and expiry periods
136
+ - View active PATs with their prefixes and expiry dates
137
+ - Revoke PATs that are no longer needed
138
+
139
+ ## Reaching the Platform API directly
140
+
141
+ The Platform exposes a versioned REST API under `/api/v1/*`. The same endpoints back the dashboard SPA, the `kici` CLI, and any third-party automation. There is no separate "public" surface — the dashboard's API is the API.
142
+
143
+ ### Base URL
144
+
145
+ | Deployment | Base URL pattern |
146
+ | ----------- | ------------------------------------------------------------------------- |
147
+ | KiCI Cloud | `https://<your-platform-host>/api/v1/` |
148
+ | Self-hosted | `https://<orchestrator-host>/<deployment-slug>/api/v1/` (slug is optional |
149
+ | | — `KICI_BASE_PATH` may add a prefix when the Platform is reverse-proxied) |
150
+
151
+ `/api/v1/*` requires authentication (see below). `/health`, `/metrics`, and `/ws` (WebSocket) sit outside that prefix and have their own access posture (`/metrics` is meant for Prometheus scrape, not public exposure).
152
+
153
+ ### Authentication
154
+
155
+ Every request to `/api/v1/*` carries an `Authorization: Bearer <token>` header. The Platform routes on the prefix:
156
+
157
+ | Prefix | Token type | Created via | Scope |
158
+ | ----------- | ----------------------------- | --------------------------------------- | ---------------- |
159
+ | `kici_pat_` | Personal access token | `kici login` or dashboard | User (cross-org) |
160
+ | `kici_sk_` | User API key | Dashboard → Settings → API keys | Org |
161
+ | `kici_sa_` | Service account key | Dashboard → Settings → Service accounts | Org |
162
+ | (other) | OIDC JWT or opaque OIDC token | OIDC login (browser SPA) | User (cross-org) |
163
+
164
+ JWT and opaque OIDC tokens are validated against the configured OIDC issuer (JWKS for JWTs, the issuer's UserInfo endpoint for opaque ones). All `kici_*` tokens are validated by SHA-256 hash lookup against the Platform DB. See [RBAC: authentication methods](https://docs.kici.dev/architecture/security/rbac/#authentication-methods) for the full model.
165
+
166
+ > **Note:** `kici_ok_` keys are **not** for the HTTP API — they authenticate orchestrator-to-Platform WebSocket connections only. Use `kici_sk_` (or `kici_pat_`) for HTTP calls.
167
+
168
+ ### Permissions
169
+
170
+ Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The 17 resources and 5 levels are documented in [RBAC](https://docs.kici.dev/architecture/security/rbac/#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
171
+
172
+ ### Configurable surfaces
173
+
174
+ The dashboard is a browser SPA on top of the same `/api/v1/*` surface, so anything you can configure in the dashboard you can configure over HTTP. The mounted route groups include:
175
+
176
+ - **Auth & identity:** `/cli/exchange-token`, `/pats`, `/user`, `/identity-links`, `/github-oauth`, `/invites`, `/invites/pending`, `/invites/:inviteId/{accept,decline}`
177
+ - **Org & membership:** `/orgs`, `/orgs/:customerId`, `/orgs/:customerId/{members,roles,api-keys,orchestrator-keys,service-accounts,billing,trust-policies}`
178
+ - **Workflows & runs:** `/orgs/:customerId/{runs,registrations,workflows,held-runs,environments,secrets,global-workflows}`
179
+ - **Webhooks & event log:** `/orgs/:customerId/{sources,webhook-endpoints,event-log}`
180
+ - **Diagnostics & activity:** `/orgs/:customerId/{diagnostics,activity,access-log}`
181
+
182
+ The full route tree is the source of truth — every method, request schema, and response schema is enumerated server-side. There is currently no auto-generated OpenAPI spec; the typed `DashboardApiType` export is the canonical contract for TypeScript clients.
183
+
184
+ ### Calling the API
185
+
186
+ Two short examples — adapt the base URL and token to your deployment.
187
+
188
+ **curl (PAT or API key):**
189
+
190
+ ```bash
191
+ TOKEN="$(grep -E '^pat=' ~/.kici/config | cut -d= -f2)" # or paste a kici_sk_…
192
+ ORG="<your-org-id>"
193
+ curl -sS \
194
+ -H "Authorization: Bearer $TOKEN" \
195
+ "https://<orchestrator-host>/<deployment-slug>/api/v1/orgs/$ORG/runs?limit=5" | jq
196
+ ```
197
+
198
+ **Browser console (after dashboard login):**
199
+
200
+ ```js
201
+ const ns = Object.keys(localStorage).find((k) => k.startsWith('oidc.user:'));
202
+ const { access_token } = JSON.parse(localStorage.getItem(ns));
203
+ const res = await fetch('/<deployment-slug>/api/v1/orgs/<your-org-id>/runs?limit=5', {
204
+ headers: { Authorization: `Bearer ${access_token}` },
205
+ });
206
+ console.log(await res.json());
207
+ ```
208
+
209
+ ### Rate limits and body size
210
+
211
+ There is currently no per-token rate limit on `/api/v1/*`. A single global body-size cap applies to webhook ingress and dashboard API requests alike.
212
+
213
+ ### Audit trail
214
+
215
+ Every `/api/v1/*` mutation that touches tenant-plane data is recorded in the upstream tenant-plane audit log, stamped with the actor (user, API key, service account, or upstream operator on a break-glass support read). Reads on customer data go through the orchestrator over the WebSocket proxy and land in the orchestrator's `access_log` table. See [Audit log](https://docs.kici.dev/operator/security/audit-log/) for the orchestrator schema and the dashboard's "Activity" page for the federated view.
216
+
217
+ ## Token storage
218
+
219
+ The CLI stores authentication data in `~/.kici/config` with `0600` permissions (owner read/write only). The config file contains:
220
+
221
+ - PAT token
222
+ - PAT expiry date
223
+ - Active organization ID
224
+ - Per-org default orchestrator clusters
225
+ - Platform endpoint URL
226
+
227
+ ## Troubleshooting
228
+
229
+ ### Browser doesn't open
230
+
231
+ If `kici login` can't open a browser:
232
+
233
+ - Use `kici login --device` for the device flow
234
+ - Or set the `KICI_BROWSER_CMD` environment variable to your browser command (e.g., `KICI_BROWSER_CMD='firefox {url}'`)
235
+
236
+ ### Device flow timeout
237
+
238
+ The device flow has a 5-minute timeout. If it expires:
239
+
240
+ - Run `kici login --device` again to get a new code
241
+ - Ensure you're using the correct URL displayed by the CLI
242
+
243
+ ### Expired PAT
244
+
245
+ If you see "Personal access token has expired":
246
+
247
+ - Run `kici login` to create a new PAT
248
+ - The old expired PAT is automatically superseded
249
+
250
+ ### "Not a member" errors
251
+
252
+ If authenticated commands return 403:
253
+
254
+ - Check your active org: `kici org current`
255
+ - List available orgs: `kici org list`
256
+ - Switch to the correct org: `kici org use <name>`
257
+
258
+ ### Connection refused
259
+
260
+ If the CLI can't reach the server:
261
+
262
+ - Verify the endpoint: check `~/.kici/config` for the correct URL
263
+ - Test connectivity: `curl <your-platform-url>/health`
264
+
265
+ ---
266
+
267
+ ## CLI reference
268
+
269
+ Source: https://docs.kici.dev/user/cli-reference/
270
+
271
+ The `@kici-dev/compiler` package provides the `kici` CLI for compiling, testing, and managing workflows.
272
+
273
+ ## Installation
274
+
275
+ ```bash
276
+ pnpm add -D @kici-dev/compiler
277
+ ```
278
+
279
+ The examples use pnpm, but npm and yarn work too — `npm install -D @kici-dev/compiler` or `yarn add -D @kici-dev/compiler`.
280
+
281
+ Run commands with `npx kici` or add scripts to your `package.json`:
282
+
283
+ ```json
284
+ {
285
+ "scripts": {
286
+ "kici:compile": "kici compile",
287
+ "kici:test": "kici test"
288
+ }
289
+ }
290
+ ```
291
+
292
+ ## Commands
293
+
294
+ ### kici compile
295
+
296
+ Compile workflows from `.kici/workflows/` to `kici.lock.json`.
297
+
298
+ ```bash
299
+ kici compile [options]
300
+ ```
301
+
302
+ **Options:**
303
+
304
+ | Option | Default | Description |
305
+ | ------------------- | ------- | -------------------------------------------- |
306
+ | `--check` | `false` | Validate workflows without writing lock file |
307
+ | `--watch` | `false` | Watch for changes and recompile |
308
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
309
+ | `--verbose` | `false` | Detailed output |
310
+
311
+ **Examples:**
312
+
313
+ ```bash
314
+ # Compile all workflows
315
+ kici compile
316
+
317
+ # Validate only (CI-friendly, no file writes)
318
+ kici compile --check
319
+
320
+ # Watch mode for development
321
+ kici compile --watch
322
+
323
+ # Custom .kici directory location
324
+ kici compile --kici-dir packages/app/.kici
325
+
326
+ # Verbose output for debugging
327
+ kici compile --verbose
328
+ ```
329
+
330
+ **Exit codes:**
331
+
332
+ | Code | Meaning |
333
+ | ---- | --------------------------- |
334
+ | 0 | Compilation successful |
335
+ | 1 | Compilation failed (errors) |
336
+
337
+ The `--check` flag is useful in CI pipelines and pre-commit hooks. It validates that workflows are syntactically and semantically correct without writing the lock file or any other files.
338
+
339
+ **Auto-type regeneration:** When authenticated (via `kici login`), `kici compile` automatically refreshes `.kici/types/secrets.d.ts` after each successful compilation. This keeps type declarations in sync with your orchestrator's secret contexts. The type regeneration is non-blocking -- if the orchestrator is unreachable, compilation still succeeds with a warning. The `--check` flag skips type regeneration since no files are written.
340
+
341
+ ### kici run
342
+
343
+ Execute workflows locally or remotely. The `run` command has two subcommands: `local` for direct execution without infrastructure, and `remote` for fixture-based execution through an orchestrator.
344
+
345
+ #### kici run local
346
+
347
+ Execute workflows locally without orchestrator infrastructure. Compiles workflows, matches triggers against the specified event, expands matrices, and runs jobs with DAG-based parallel scheduling.
348
+
349
+ ```bash
350
+ kici run local [event] [options]
351
+ ```
352
+
353
+ **Arguments:**
354
+
355
+ | Argument | Required | Description |
356
+ | -------- | ---------------------- | ------------------------------------------------ |
357
+ | `event` | when `--pick` is unset | Event type (e.g., `push`, `pr:open`, `schedule`) |
358
+
359
+ **Options:**
360
+
361
+ | Option | Default | Description |
362
+ | --------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
363
+ | `-p, --pick` | `false` | Interactively pick a workflow + trigger (see below) |
364
+ | `--workflow <name>` | none | Run only the specified workflow (mutex with `--pick`) |
365
+ | `--job <name>` | none | Run only the specified job (and its dependencies) |
366
+ | `--branch <name>` | detected | Override detected git branch |
367
+ | `--sha <hash>` | detected | Override detected git SHA |
368
+ | `--payload <path>` | none | Path to explicit event payload JSON file |
369
+ | `--concurrency <n>` | CPU cores | Max parallel jobs **within one run** (job-level only). Cross-run [concurrency groups](https://docs.kici.dev/user/concurrency/) declared in `workflow({ concurrency: ... })` are enforced separately — see "Concurrency enforcement" below. |
370
+ | `--keep-going` | `false` | Continue after job failure |
371
+ | `--container` | `false` | Use Podman container isolation |
372
+ | `--env <KEY=VALUE>` | none | Environment variable override (repeatable) |
373
+ | `--input <KEY=VALUE>` | none | Typed workflow-dispatch input (repeatable) — coerced + validated against the workflow's `dispatch({ inputs })` schema, exposed as `ctx.dispatchInputs` (see [triggers → typed dispatch inputs](https://docs.kici.dev/user/sdk/triggers/#typed-dispatch-inputs)) |
374
+ | `--files <path>` | git diff | Override changed file paths (repeatable, default: git diff) |
375
+ | `--quiet` | `false` | Suppress streaming output (summary only) |
376
+ | `--json` | `false` | Output structured JSON result |
377
+ | `--junit <path>` | none | Output JUnit XML result to file |
378
+ | `--debug` | `false` | Verbose internals |
379
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
380
+ | `--in-place` | `false` | Run against the real working directory instead of an isolated tmp checkout (see "Execution isolation" below) |
381
+ | `--keep` | `false` | Always retain the isolated tmp checkout (default: keep only on failure) |
382
+
383
+ **Interactive workflow selection (`--pick` / `-p`):**
384
+
385
+ When you do not remember the event arg for a workflow, pass `--pick` (or `-p`) to open an interactive picker. It lists every workflow with a compact summary of its triggers, lets you choose one, and (for multi-trigger workflows) prompts again for which trigger to simulate. The selected trigger is converted back into an event arg and fed through the normal pipeline.
386
+
387
+ ```bash
388
+ # Open the picker across all triggerable workflows
389
+ kici run local --pick
390
+
391
+ # Scope the picker to a trigger family (e.g. only workflows that react to pr:*)
392
+ kici run local pr:open --pick
393
+ ```
394
+
395
+ Rules:
396
+
397
+ - `--pick` is mutually exclusive with `--workflow`. Passing both exits with code 2.
398
+ - When `stdin` is not a TTY, `--pick` prints the available workflows and exits without running anything — fall back to `kici run local <event> --workflow <name>` in scripts.
399
+ - Passing an event arg together with `--pick` narrows the picker to workflows that declare at least one trigger in that event family (e.g. `schedule --pick` shows only scheduled workflows).
400
+
401
+ **Concurrency enforcement:**
402
+
403
+ When the workflow declares a `concurrency` block, `kici run local` enforces it across concurrent local invocations on the same machine and user account. The behavior mirrors the orchestrator:
404
+
405
+ - The `group` callback is evaluated against the simulated event (same `{ branch, event }` context that the agent sees), and the resulting key is used as the lock identity. Throwing from `group` aborts the workflow run with a clear error — there is no fallback to the workflow name.
406
+ - `cancelInProgress: true` interrupts the holder via `SIGTERM`, then escalates to `SIGKILL` after a grace window if the holder does not exit, and proceeds with the new run.
407
+ - Otherwise the new invocation waits in FIFO order. A status line is printed when the wait starts and roughly every five seconds thereafter.
408
+ - Locks live under `$XDG_RUNTIME_DIR/kici-local-locks/` on Linux, falling back to `os.tmpdir()/kici-local-locks-<uid>/` on platforms without a per-user runtime dir. Each lock file records the holder PID, hostname, workflow name, group key, and start timestamp so concurrent invocations can describe what they are waiting for.
409
+ - Stale locks (the recorded holder PID is gone, per `process.kill(pid, 0)`) are reclaimed automatically.
410
+
411
+ Coordination is local only — running the same workflow on two different machines does not serialize across them. That requires the orchestrator.
412
+
413
+ The `SIGTERM`-to-`SIGKILL` grace window defaults to 30 000 ms. Override it with the `KICI_LOCAL_LOCK_KILL_GRACE_MS` environment variable (positive integer, milliseconds) when iterating on workflows that need longer to clean up on cancellation.
414
+
415
+ **Execution isolation:**
416
+
417
+ By default, `kici run local` executes steps inside an **isolated tmp checkout** rather than against your real working directory. Any file a step writes, builds, or deletes — and any `git` mutation a step performs — lands in that throwaway copy, so casual local runs never touch your tree.
418
+
419
+ What gets materialized into the isolated checkout has full parity with what `kici run remote` reconstructs: your current working tree minus gitignored files, with `.kiciignore` applied to local changes, over a real `.git` directory. Concretely, the checkout is built from a clone pinned to your current `HEAD`, with your local overlay (modified, staged, and untracked-but-not-ignored files) copied on top and locally-deleted files removed. Workflows that read git metadata work because the `.git` directory is present and pinned to your `HEAD`.
420
+
421
+ The path is logged at run start (for example, `running in /tmp/kici-run-ab12cd`) so you can inspect it.
422
+
423
+ Cleanup policy:
424
+
425
+ - On a fully successful run, the isolated checkout is removed.
426
+ - On failure, it is retained and its path is logged so you can inspect the failed state.
427
+ - `--keep` always retains it, even on success.
428
+ - Retained checkouts are garbage-collected after 72 hours by the next `kici run local` invocation — copy a checkout elsewhere if you need it longer.
429
+
430
+ Set the `KICI_RUN_DIR` environment variable to place the isolated checkout under a base directory other than the system temp directory.
431
+
432
+ Secrets are always sourced from your real `.kici/` directory, not from the isolated checkout. Gitignored secret files (such as `.kici/.env.local` and `.kici/secrets.yaml`) are never copied into the checkout, so a step that reads a secret still gets it from the original location.
433
+
434
+ Pass `--in-place` to run against the real working directory instead — useful when you explicitly want in-tree execution. `--in-place` requires no git repository; the default isolated mode does, and fails with an actionable error pointing at `--in-place` when the directory is not a git repository.
435
+
436
+ **Examples:**
437
+
438
+ ```bash
439
+ # Run workflows matching a push event
440
+ kici run local push
441
+
442
+ # Run only a specific workflow
443
+ kici run local push --workflow ci
444
+
445
+ # Run only a specific job (and its dependencies)
446
+ kici run local push --job test
447
+
448
+ # JSON output for CI scripting
449
+ kici run local push --json
450
+
451
+ # JUnit XML for CI integration
452
+ kici run local push --junit results.xml
453
+
454
+ # Quiet mode (summary only, no streaming)
455
+ kici run local push --quiet
456
+
457
+ # Override branch and SHA
458
+ kici run local push --branch main --sha abc1234
459
+
460
+ # Environment variable overrides
461
+ kici run local push --env NODE_ENV=test --env CI=true
462
+
463
+ # Continue running other jobs after one fails
464
+ kici run local push --keep-going
465
+ ```
466
+
467
+ **Exit codes:**
468
+
469
+ | Code | Meaning |
470
+ | ---- | ----------------------- |
471
+ | 0 | All workflows succeeded |
472
+ | 1 | One or more jobs failed |
473
+
474
+ **Output formats:**
475
+
476
+ - **Default:** Streaming job output during execution, followed by a tree-format summary with per-step timing
477
+ - **`--json`:** Structured JSON with workflows, jobs, steps, timing, and matrix values
478
+ - **`--junit <path>`:** Standard JUnit XML for CI integration (Jenkins, GitLab, etc.)
479
+ - **`--quiet`:** Summary only, no streaming output during execution
480
+
481
+ #### kici run remote
482
+
483
+ Execute fixtures remotely through the full CI pipeline. Fixtures are defined in `.kici/tests/*.ts` using the `fixture()` factory function. Without arguments, lists available fixtures.
484
+
485
+ Remote runs route through the Platform. Authenticate with a personal access token (`kici login`), then target an organization with `kici org use <org>` or the `--org` flag. The Platform relays the run to the org's orchestrator, while your working-tree overlay uploads directly to object storage — see [How the run is routed](https://docs.kici.dev/user/cli-reference/#how-the-run-is-routed) and [The two planes](https://docs.kici.dev/user/cli-reference/#the-two-planes) below.
486
+
487
+ Like `kici run local`, `kici run remote` recompiles your workflows (`.kici/workflows` → `kici.lock.json`) before dispatching, so the orchestrator matches and dispatches against your current workflow definitions — a brand-new or edited workflow takes effect without a separate `kici compile`. A compile or validation error aborts the run before anything is uploaded.
488
+
489
+ The orchestrator must have **cache storage configured** (`KICI_STORAGE_TYPE` = `s3` or `filesystem`) with a dev-reachable upload endpoint so the CLI's direct upload succeeds; see the [testing guide](https://docs.kici.dev/user/testing-guide/) and [Storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/) for setup.
490
+
491
+ ```bash
492
+ kici run remote [fixture] [options]
493
+ ```
494
+
495
+ **Arguments:**
496
+
497
+ | Argument | Required | Description |
498
+ | --------- | -------- | ----------------------------------------------- |
499
+ | `fixture` | no | Fixture name or glob pattern (omit to list all) |
500
+
501
+ **Options:**
502
+
503
+ | Option | Default | Description |
504
+ | --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
505
+ | `--org <id>` | active | Target organization for this run (overrides `kici org use`) |
506
+ | `--orchestrator <name>` | default | Target orchestrator cluster within the org (overrides the per-org default) |
507
+ | `--all` | `false` | Run all fixtures |
508
+ | `-p, --pick` | `false` | Interactively pick fixtures to run (multi-select; mutex with a fixture arg, `--all`, and `--workflow`) |
509
+ | `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
510
+ | `--parallel` | `false` | Run multiple fixtures concurrently |
511
+ | `--no-wait` | - | Fire and forget (print runIds, don't stream) |
512
+ | `--quiet` | `false` | Minimal output (only final result) |
513
+ | `--json` | `false` | Machine-readable JSON output |
514
+ | `--junit <path>` | none | JUnit XML output to file for CI integration |
515
+ | `--history` | `false` | Show table of recent test runs |
516
+ | `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted (repeatable) |
517
+ | `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted (repeatable) — see [testing guide](https://docs.kici.dev/user/testing-guide/) |
518
+ | `--input <KEY=VALUE>` | none | Typed workflow-dispatch input (repeatable) — validated + coerced + defaulted on the orchestrator from the lock descriptor, exposed as `ctx.dispatchInputs` (see [triggers → typed dispatch inputs](https://docs.kici.dev/user/sdk/triggers/#typed-dispatch-inputs)) |
519
+ | `--target <selector>` | none | Narrow `runsOnAll` jobs to hosts matching this label selector (repeatable, AND-combined) |
520
+ | `--target-allow-empty` | `false` | A `--target` that narrows a `runsOnAll` job to zero hosts skips it instead of failing |
521
+ | `--debug` | `false` | Verbose internals |
522
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
523
+
524
+ **Examples:**
525
+
526
+ ```bash
527
+ # List available fixtures
528
+ kici run remote
529
+
530
+ # Run a single fixture against the active org
531
+ kici run remote push-main
532
+
533
+ # Target a specific org for this run
534
+ kici run remote push-main --org xyz789ghi012
535
+
536
+ # Target a specific orchestrator cluster within the org
537
+ kici run remote push-main --orchestrator us-east
538
+
539
+ # Run all push-related fixtures
540
+ kici run remote 'push-*'
541
+
542
+ # Run everything
543
+ kici run remote --all
544
+
545
+ # Run a specific workflow directly (bypass trigger matching)
546
+ kici run remote --workflow ci
547
+
548
+ # Quiet mode -- just pass/fail
549
+ kici run remote push-main --quiet
550
+
551
+ # JSON output for scripting
552
+ kici run remote push-main --json
553
+
554
+ # Fire and forget
555
+ kici run remote push-main --no-wait
556
+
557
+ # View recent test run history
558
+ kici run remote --history
559
+
560
+ # Interactively pick which fixtures to run (multi-select)
561
+ kici run remote --pick
562
+
563
+ # Narrow runsOnAll jobs to a subset of the host roster
564
+ kici run remote deploy --target role:web
565
+
566
+ # AND-combine repeated --target values (hosts must match every selector)
567
+ kici run remote deploy --target role:web --target dc:eu
568
+
569
+ # Skip a runsOnAll job instead of failing it when the target matches no host
570
+ kici run remote deploy --target role:gpu --target-allow-empty
571
+ ```
572
+
573
+ **Interactive fixture selection (`--pick` / `-p`):**
574
+
575
+ Pass `--pick` (or `-p`) to open an interactive checkbox menu of the available
576
+ fixtures. Toggle one or more with space, confirm with enter, and the selected
577
+ fixtures run through the normal remote pipeline (honoring `--parallel`,
578
+ `--no-wait`, and the other run flags). Notes:
579
+
580
+ - `--pick` is mutually exclusive with a fixture argument, `--all`, and
581
+ `--workflow`. Passing any together exits with code 2.
582
+ - When `stdin` is not a TTY, `--pick` prints the available fixtures and exits
583
+ without running anything — pass a fixture name (or `--all`) in scripts.
584
+
585
+ #### Host narrowing with `--target`
586
+
587
+ `--target <selector>` is a runtime narrowing for `runsOnAll` jobs, analogous to
588
+ Ansible's `--limit`. A `runsOnAll` job normally fans out to **every** roster host
589
+ matching its predicate, one pinned execution per host. `--target` intersects that
590
+ matched roster with a label selector, so the effective host set is
591
+ `runsOnAll ∩ target`:
592
+
593
+ - **Narrow-only.** `--target` can only _remove_ hosts from the matched set, never
594
+ add them. The widening dimension (OR across host groups) lives in the workflow's
595
+ `runsOnAll`; `--target` only subtracts.
596
+ - **Run-global, `runsOnAll`-only.** A single `--target` applies to every
597
+ `runsOnAll` job in the run. Jobs pinned to a single host with `runsOn` are
598
+ untouched.
599
+ - **Repeatable and AND-combined.** Each `--target` value is its own selector; a
600
+ host must satisfy **all** of them to survive the narrowing. Use a single value
601
+ for an OR-style match within one selector and repeated values for AND.
602
+ - **Selector syntax** matches `runsOn`: an exact label (`role:web`), a glob
603
+ (`role:*`), or a regex (`/^box-0[1-3]$/`).
604
+
605
+ When `--target` narrows a `runsOnAll` job to zero hosts, the default is to **fail**
606
+ the run (fail-loud — a typo in the selector shouldn't silently skip work). Pass
607
+ `--target-allow-empty` to **skip** the zeroed job instead; the job records a
608
+ `skipped` status, and any downstream job that needs it with `when: 'on-skip'` (or
609
+ `when: 'always'`) still runs. See [Job dependencies](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs)
610
+ for the `when` gating model.
611
+
612
+ **Exit codes:**
613
+
614
+ | Code | Meaning |
615
+ | ---- | ---------------------------- |
616
+ | 0 | All matched workflows passed |
617
+ | 1 | One or more workflows failed |
618
+
619
+ #### How the run is routed
620
+
621
+ A remote run is dispatched to your **active organization** — the one set with `kici org use <org>`, or overridden per-run with `--org <id>`. The org is resolved in this order:
622
+
623
+ 1. The `--org <id>` flag, if provided.
624
+ 2. Otherwise the active org saved in your global config by `kici org use <org>`.
625
+ 3. If neither is set, the command errors and asks you to select an org with `kici org use` or pass `--org`.
626
+
627
+ The orchestrator anchors the org without any manual webhook source: it auto-provisions a system-managed **remote source** (routing key `remote:<orgId>`) that maps to its bound organization, so even a zero-source org is immediately routable for remote runs. You never set a routing key for a remote run — selecting the org is enough.
628
+
629
+ When an org has more than one connected orchestrator cluster, the CLI picks the target cluster in this order:
630
+
631
+ 1. The `--orchestrator <name>` flag, if provided.
632
+ 2. Otherwise the per-org default cluster, set with `kici orchestrators use <name>`.
633
+ 3. If the org has exactly **one** connected orchestrator, it is auto-selected.
634
+ 4. Otherwise the run errors with the list of connected clusters, and you pass `--orchestrator <name>` to choose one. Run `kici orchestrators list` to see the available cluster names.
635
+
636
+ #### The two planes
637
+
638
+ `kici run remote` uses two independent paths:
639
+
640
+ - **Control plane** — run initiation, trigger, status, log retrieval, and cancellation flow from your machine through the Platform, which relays them over a WebSocket connection to the org's orchestrator. Logs are delivered by the CLI polling the Platform for log chunks (tracked by a monotonic line cursor) and run status until the run reaches a terminal state; there is no direct streaming socket to the orchestrator.
641
+ - **Data plane** — your working-tree overlay tarball uploads **directly** from your machine to the orchestrator's object store via a presigned PUT URL. The overlay never passes through the Platform. This is why the orchestrator's object-store upload endpoint must be reachable from your machine; see [Storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/).
642
+
643
+ An orchestrator with no Platform connection cannot serve remote runs — the Platform is the service that offers them. For executing workflow steps on your own machine without an orchestrator (no scaler, agents, or environments), use [`kici run local`](https://docs.kici.dev/user/cli-reference/#kici-run-local).
644
+
645
+ #### Fresh repos (no GitHub remote)
646
+
647
+ `kici run remote` works even if the repo has never been pushed to GitHub. When no remote is detected:
648
+
649
+ - The entire repo content is uploaded (not just a diff overlay)
650
+ - The lock file is sent inline (no GitHub API fetch)
651
+ - Steps that use git commands will fail (no `.git` directory in the remote workspace)
652
+ - Build cache (`__build__` jobs) is skipped for local repos
653
+ - Environments must have `allowLocalExecution: true` to be accessible from local runs (default is `false`)
654
+
655
+ Destination routing is unchanged for fresh repos: the run still goes to your active org through the Platform.
656
+
657
+ For a detailed guide on writing fixtures, configuring secrets, and understanding the upload flow, see [Testing guide](https://docs.kici.dev/user/testing-guide/).
658
+
659
+ #### kici orchestrators
660
+
661
+ List the orchestrator clusters connected to an organization, and set the per-org default cluster used by `kici run remote`. Requires `kici login` and an active org (or pass `--org`).
662
+
663
+ ```bash
664
+ kici orchestrators list [--org <id>]
665
+ kici orchestrators use <clusterName> [--org <id>]
666
+ ```
667
+
668
+ **`kici orchestrators list`** prints the org's connected orchestrator clusters, so you know what to pass to `--orchestrator` (or to `kici orchestrators use`).
669
+
670
+ **`kici orchestrators use <clusterName>`** sets the default orchestrator cluster for the org, stored per-org in your global config. Subsequent `kici run remote` invocations target that cluster unless overridden with `--orchestrator`.
671
+
672
+ **Examples:**
673
+
674
+ ```bash
675
+ # List the active org's connected clusters
676
+ kici orchestrators list
677
+
678
+ # List a specific org's clusters
679
+ kici orchestrators list --org xyz789ghi012
680
+
681
+ # Set the default cluster for the active org
682
+ kici orchestrators use us-east
683
+
684
+ # Set the default cluster for a specific org
685
+ kici orchestrators use us-east --org xyz789ghi012
686
+ ```
687
+
688
+ ### kici test
689
+
690
+ Preview which workflows match a trigger event (dry-run, no execution). Useful for verifying trigger configurations during development.
691
+
692
+ ```bash
693
+ kici test [event] [options]
694
+ ```
695
+
696
+ **Arguments:**
697
+
698
+ | Argument | Required | Description |
699
+ | -------- | -------- | ----------------------------------------------------------- |
700
+ | `event` | no | Event type to preview (e.g., `push`, `pr:open`, `schedule`) |
701
+
702
+ **Options:**
703
+
704
+ | Option | Default | Description |
705
+ | --------------------------- | ------- | ------------------------------------------------------------ |
706
+ | `--workflow <name>` | none | Filter to specific workflow |
707
+ | `--job <name>` | none | Filter to specific job |
708
+ | `--branch <name>` | `main` | Override target branch for trigger matching |
709
+ | `--sha <hash>` | none | Override commit SHA |
710
+ | `--files <path>` | none | Simulate changed file path for trigger matching (repeatable) |
711
+ | `--secret <key=value>` | none | Inject flat secret (repeatable) |
712
+ | `--context <ctx.key=value>` | none | Inject context secret (repeatable) |
713
+ | `--debug` | `false` | Verbose internals |
714
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
715
+
716
+ **Examples:**
717
+
718
+ ```bash
719
+ # Preview which workflows match a push event
720
+ kici test push
721
+
722
+ # Preview PR trigger matching
723
+ kici test pr:open
724
+
725
+ # Preview with branch override
726
+ kici test push --branch develop
727
+
728
+ # Filter to specific workflow
729
+ kici test push --workflow ci
730
+
731
+ # Simulate changed files for path-filtered triggers
732
+ kici test push --files src/index.ts --files README.md
733
+ ```
734
+
735
+ **Exit codes:**
736
+
737
+ | Code | Meaning |
738
+ | ---- | ------------------------------------------ |
739
+ | 0 | Preview completed (including zero matches) |
740
+ | 1 | Error |
741
+
742
+ **Migration from old `kici test <fixture>`:** If you were using `kici test <fixture-name>` for remote fixture execution, use `kici run remote <fixture-name>` instead. For local workflow execution, use `kici run local <event>`.
743
+
744
+ ### kici login
745
+
746
+ Authenticate with KiCI via browser-based OAuth (default) or API key (`--token`).
747
+
748
+ By default, `kici login` opens your browser for OIDC authentication using PKCE. In headless environments (SSH, CI, containers), it automatically switches to the RFC 8628 device authorization flow where you visit a URL and enter a code.
749
+
750
+ After OAuth, the CLI exchanges the OIDC token for a personal access token (PAT) stored in the config directory (`~/.kici/config` by default, overridable with `KICI_CONFIG_DIR`).
751
+
752
+ `kici login` targets the hosted KiCI Platform by default. To authenticate against another environment (a self-hosted Platform, for example), pass `--platform-endpoint` / `--oidc-issuer` or set `KICI_PLATFORM_URL` / `KICI_OIDC_ISSUER`. Login persists the platform endpoint and OIDC issuer it authenticated against alongside the PAT, so a saved PAT always matches its endpoint. Because the config describes one environment at a time, **switching the endpoint resets the active organization and default clusters** — re-run `kici org use <name>` after switching environments.
753
+
754
+ ```bash
755
+ kici login [options]
756
+ ```
757
+
758
+ **Options:**
759
+
760
+ | Option | Default | Description |
761
+ | --------------------------- | ------- | --------------------------------------------------- |
762
+ | `--token <key>` | none | API key for direct authentication (legacy) |
763
+ | `--device` | false | Force device authorization flow (headless/SSH) |
764
+ | `--platform-endpoint <url>` | none | Platform relay URL |
765
+ | `--oidc-issuer <url>` | none | OIDC issuer URL (selects a non-default environment) |
766
+ | `--routing-key <key>` | none | Routing key for webhook source identification |
767
+
768
+ **Environment variables:**
769
+
770
+ | Variable | Default | Description |
771
+ | --------------------- | -------------------------------------------- | ---------------------------------------------------------------------- |
772
+ | `KICI_PLATFORM_URL` | `https://api.kici.dev` | Platform API base URL (override for a self-hosted Platform) |
773
+ | `KICI_OIDC_ISSUER` | `https://auth.kici.dev/realms/kici-internal` | OIDC issuer URL (override for a self-hosted Platform) |
774
+ | `KICI_OIDC_CLIENT_ID` | `kici-cli` | OIDC client ID (override for a self-hosted Platform) |
775
+ | `KICI_BROWSER_CMD` | uses `open` package | Custom browser command with `{url}` placeholder, or `none` to suppress |
776
+ | `KICI_CALLBACK_PORT` | random | Fixed port for OAuth PKCE callback server |
777
+ | `KICI_CONFIG_DIR` | `~/.kici` | Override config directory |
778
+
779
+ **Examples:**
780
+
781
+ ```bash
782
+ # Browser-based OAuth login (default)
783
+ kici login
784
+
785
+ # Force device flow (for SSH/headless)
786
+ kici login --device
787
+
788
+ # Legacy API key login
789
+ kici login --token kici_sk_abc123...
790
+
791
+ # Log in against a self-hosted Platform
792
+ kici login --platform-endpoint https://platform.example.com \
793
+ --oidc-issuer https://auth.example.com/realms/kici-internal
794
+
795
+ # Suppress browser opening (print authorize URL to stdout)
796
+ KICI_BROWSER_CMD=none kici login
797
+
798
+ # Use custom browser command
799
+ KICI_BROWSER_CMD='firefox {url}' kici login
800
+
801
+ # Fixed callback port and custom config directory
802
+ KICI_CALLBACK_PORT=19876 KICI_CONFIG_DIR=/tmp/kici-test kici login
803
+ ```
804
+
805
+ **Headless detection:** The CLI automatically detects headless environments by checking for `SSH_CLIENT`, `SSH_TTY`, `CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `container`, or `DOCKER_CONTAINER` env vars, and on Linux, the absence of `DISPLAY` and `WAYLAND_DISPLAY`.
806
+
807
+ ### kici logout
808
+
809
+ Revoke your personal access token on the server and clear local credentials.
810
+
811
+ If the server is unreachable, local credentials are still cleared (the PAT will expire automatically). Non-auth config fields (active org, default clusters, etc.) are preserved.
812
+
813
+ ```bash
814
+ kici logout
815
+ ```
816
+
817
+ **Examples:**
818
+
819
+ ```bash
820
+ # Log out and revoke PAT
821
+ kici logout
822
+ ```
823
+
824
+ ### kici org
825
+
826
+ Manage organization context. Requires a PAT (run `kici login` first).
827
+
828
+ #### kici org list
829
+
830
+ List organizations you belong to. The active org is marked with a star (`*`).
831
+
832
+ ```bash
833
+ kici org list
834
+ ```
835
+
836
+ **Example output:**
837
+
838
+ ```
839
+ Organizations:
840
+
841
+ * Personal (owner) abc123def456
842
+ My team (admin) xyz789ghi012
843
+ ```
844
+
845
+ #### kici org use
846
+
847
+ Switch the active organization by name (case-insensitive) or ID.
848
+
849
+ ```bash
850
+ kici org use <name>
851
+ ```
852
+
853
+ **Arguments:**
854
+
855
+ | Argument | Required | Description |
856
+ | -------- | -------- | ----------------------- |
857
+ | `name` | yes | Organization name or ID |
858
+
859
+ **Examples:**
860
+
861
+ ```bash
862
+ # Switch by name
863
+ kici org use "My team"
864
+
865
+ # Switch by ID
866
+ kici org use xyz789ghi012
867
+ ```
868
+
869
+ #### kici org current
870
+
871
+ Show the current active organization.
872
+
873
+ ```bash
874
+ kici org current
875
+ ```
876
+
877
+ ### kici diagnostics
878
+
879
+ Show the orchestrators, scalers, and agents serving your organization — the
880
+ terminal equivalent of the dashboard Diagnostics page. Reads the same
881
+ org-scoped data the dashboard does, so it needs `kici login` and an active org
882
+ (`kici org use <name>`).
883
+
884
+ The output has three parts: a one-line header (runs in the last 24h, success
885
+ rate, average duration, queued/running job counts), any infrastructure alerts
886
+ (only shown when present), and a tree of each orchestrator with its scalers and
887
+ agents. Each agent line shows its labels, platform/architecture, active/maximum
888
+ concurrency, and heartbeat age.
889
+
890
+ ```bash
891
+ kici diagnostics [options]
892
+ ```
893
+
894
+ **Options:**
895
+
896
+ | Option | Default | Description |
897
+ | --------------------- | ------- | --------------------------------------------------- |
898
+ | `--json` | `false` | Machine-readable JSON output |
899
+ | `--verbose` | `false` | Show extended per-agent fields (host, node, memory) |
900
+ | `--orchestrator <id>` | all | Scope the tree to one orchestrator connection id |
901
+
902
+ **Examples:**
903
+
904
+ ```bash
905
+ # Show the full infrastructure tree
906
+ kici diagnostics
907
+
908
+ # Extended per-agent detail
909
+ kici diagnostics --verbose
910
+
911
+ # Only one orchestrator's scalers and agents
912
+ kici diagnostics --orchestrator conn-abc123
913
+
914
+ # Machine-readable output
915
+ kici diagnostics --json
916
+ ```
917
+
918
+ ### kici runs
919
+
920
+ Inspect and manage execution runs from the terminal — the equivalent of the
921
+ dashboard Runs page. All `kici runs` subcommands read/write the same org-scoped
922
+ data as the dashboard, so they require `kici login` and an active org
923
+ (`kici org use <name>`).
924
+
925
+ #### kici runs list
926
+
927
+ List runs with optional filters. Output is a table (run id, workflow, status,
928
+ branch, trigger, started, duration); pagination is reported at the bottom.
929
+
930
+ ```bash
931
+ kici runs list [options]
932
+ ```
933
+
934
+ **Options:**
935
+
936
+ | Option | Default | Description |
937
+ | ----------------------- | ------- | --------------------------------------------- |
938
+ | `--status <s>` | all | Filter by run status |
939
+ | `--workflow <w>` | all | Filter by workflow name |
940
+ | `--branch <b>` | all | Filter by branch/ref |
941
+ | `--repo <r>` | all | Filter by repository |
942
+ | `--trigger <t>` | all | Filter by trigger type |
943
+ | `--source <routingKey>` | all | Filter by source routing key |
944
+ | `--since <ts>` | none | Only runs since this ISO-8601 or epoch ms |
945
+ | `--page <n>` | `1` | Page number (server page size is fixed at 20) |
946
+ | `--json` | `false` | Machine-readable JSON output |
947
+
948
+ ```bash
949
+ kici runs list
950
+ kici runs list --status running
951
+ kici runs list --workflow ci --branch main
952
+ kici runs list --json | jq '.runs[].runId'
953
+ ```
954
+
955
+ #### kici runs show
956
+
957
+ Show a run's summary header plus its jobs-and-steps tree (name, status,
958
+ duration, exit code). If the run id is not on the Platform but exists in your
959
+ local run history (from `kici run local`), the local record is shown instead.
960
+
961
+ ```bash
962
+ kici runs show <run-id> [options]
963
+ ```
964
+
965
+ | Option | Default | Description |
966
+ | -------- | ------- | ---------------------------- |
967
+ | `--json` | `false` | Machine-readable JSON output |
968
+
969
+ ```bash
970
+ kici runs show abc123
971
+ kici runs show abc123 --json
972
+ ```
973
+
974
+ #### kici runs logs
975
+
976
+ Print each job/step's log lines in order, with headers.
977
+
978
+ ```bash
979
+ kici runs logs <run-id> [options]
980
+ ```
981
+
982
+ | Option | Default | Description |
983
+ | -------------- | ------- | -------------------------------------- |
984
+ | `--job <name>` | all | Only print logs for this job |
985
+ | `-f, --follow` | `false` | Tail logs for a live run until it ends |
986
+ | `--json` | `false` | Machine-readable JSON output |
987
+
988
+ ```bash
989
+ kici runs logs abc123
990
+ kici runs logs abc123 --job build
991
+ kici runs logs abc123 --follow
992
+ ```
993
+
994
+ #### kici runs rerun
995
+
996
+ Re-trigger a completed run. Prints the new run id. The server enforces a short
997
+ cooldown between reruns of the same run.
998
+
999
+ ```bash
1000
+ kici runs rerun <run-id> [options]
1001
+ ```
1002
+
1003
+ | Option | Default | Description |
1004
+ | -------- | ------- | ---------------------------- |
1005
+ | `--json` | `false` | Machine-readable JSON output |
1006
+
1007
+ ```bash
1008
+ kici runs rerun abc123
1009
+ ```
1010
+
1011
+ #### kici runs cancel
1012
+
1013
+ Cancel a single run, or all in-progress runs on a branch.
1014
+
1015
+ ```bash
1016
+ kici runs cancel [run-id] [options]
1017
+ ```
1018
+
1019
+ | Argument | Required | Description |
1020
+ | -------- | -------- | ---------------- |
1021
+ | `run-id` | no | Run ID to cancel |
1022
+
1023
+ | Option | Default | Description |
1024
+ | ----------------- | ------- | ------------------------------------------- |
1025
+ | `--force` | `false` | Force cancel (kill immediately, skip hooks) |
1026
+ | `--branch <name>` | none | Cancel all in-progress runs on this branch |
1027
+
1028
+ ```bash
1029
+ kici runs cancel abc123
1030
+ kici runs cancel abc123 --force
1031
+ kici runs cancel --branch feature/wip
1032
+ ```
1033
+
1034
+ When `--json` is set on any of these commands, `kici` emits only the JSON
1035
+ document on stdout — the `kici v<version>` banner is suppressed — so the output
1036
+ is safe to pipe into `jq` or `JSON.parse`. The same holds for the other
1037
+ `--json` commands (`kici run remote --json`, `kici workflows list --json`) and
1038
+ for `--quiet`.
1039
+
1040
+ ### kici approve
1041
+
1042
+ Approve a held [approval gate](https://docs.kici.dev/user/approvals/) so the run resumes. Identify the held element by run ID, optionally narrowed to a job and step.
1043
+
1044
+ ```bash
1045
+ kici approve <run-id> [options]
1046
+ ```
1047
+
1048
+ **Arguments:**
1049
+
1050
+ | Argument | Required | Description |
1051
+ | -------- | -------- | ---------------------------------- |
1052
+ | `run-id` | yes | Run ID holding the gate to approve |
1053
+
1054
+ **Options:**
1055
+
1056
+ | Option | Default | Description |
1057
+ | ---------------- | ------- | ---------------------------------------------------- |
1058
+ | `--job <name>` | none | Approve a held job (omit for a workflow-level hold) |
1059
+ | `--step <index>` | none | Approve a held step by its index (used with `--job`) |
1060
+
1061
+ **Examples:**
1062
+
1063
+ ```bash
1064
+ # Approve a workflow-level hold
1065
+ kici approve abc123
1066
+
1067
+ # Approve a held job
1068
+ kici approve abc123 --job deploy-production
1069
+
1070
+ # Approve a held step (steps are addressed by index)
1071
+ kici approve abc123 --job migrate-and-deploy --step 1
1072
+ ```
1073
+
1074
+ You must be eligible for at least one unsatisfied clause (a member of a named team, or a named user) and hold the `environments:write` or `ci_trust:write` permission. The command reports whether the element was released, how many clauses remain, or that it was rejected.
1075
+
1076
+ ### kici reject
1077
+
1078
+ Reject a held [approval gate](https://docs.kici.dev/user/approvals/). A rejection fails the held element and the run. A reason is required.
1079
+
1080
+ ```bash
1081
+ kici reject <run-id> --reason <text> [options]
1082
+ ```
1083
+
1084
+ **Arguments:**
1085
+
1086
+ | Argument | Required | Description |
1087
+ | -------- | -------- | --------------------------------- |
1088
+ | `run-id` | yes | Run ID holding the gate to reject |
1089
+
1090
+ **Options:**
1091
+
1092
+ | Option | Default | Description |
1093
+ | ----------------- | ------- | --------------------------------------------------- |
1094
+ | `--reason <text>` | none | Required. Reason recorded with the rejection |
1095
+ | `--job <name>` | none | Reject a held job (omit for a workflow-level hold) |
1096
+ | `--step <index>` | none | Reject a held step by its index (used with `--job`) |
1097
+
1098
+ **Examples:**
1099
+
1100
+ ```bash
1101
+ # Reject a held job with a reason
1102
+ kici reject abc123 --job deploy-production --reason "Wrong release branch"
1103
+ ```
1104
+
1105
+ ### kici secrets list
1106
+
1107
+ List secret contexts available for test runs. Shows context names and key names (not values).
1108
+
1109
+ ```bash
1110
+ kici secrets list
1111
+ ```
1112
+
1113
+ Each "context" corresponds to an environment configured on the orchestrator. The output lists every environment whose `allowLocalExecution` flag is `true` (the gate that lets CLI-initiated test runs resolve secrets through that environment), along with the secret key names reachable from the environment's bound scopes.
1114
+
1115
+ Only key names are shown — secret values are never returned over this endpoint.
1116
+
1117
+ **Prerequisites:** authenticate via `kici login` and select an active organization with `kici org use <name>`.
1118
+
1119
+ ### kici types
1120
+
1121
+ Generate TypeScript declaration files from orchestrator environment metadata. The generated `.d.ts` file augments the SDK's `KnownSecretKeys` and `EnvironmentSecrets` interfaces, providing compile-time autocomplete and type checking for secret key names.
1122
+
1123
+ ```bash
1124
+ kici types [options]
1125
+ ```
1126
+
1127
+ **Options:**
1128
+
1129
+ | Option | Default | Description |
1130
+ | ------------------- | ------- | ----------------------- |
1131
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
1132
+
1133
+ **Prerequisites:** Must be authenticated via `kici login`.
1134
+
1135
+ **Output:** `.kici/types/secrets.d.ts`
1136
+
1137
+ **Examples:**
1138
+
1139
+ ```bash
1140
+ # Generate types from orchestrator
1141
+ kici types
1142
+
1143
+ # Use custom .kici directory
1144
+ kici types --kici-dir packages/app/.kici
1145
+ ```
1146
+
1147
+ **How it works:**
1148
+
1149
+ 1. Fetches all environment metadata (environment names and secret key names) from the orchestrator
1150
+ 2. Generates a `.d.ts` file that augments `@kici-dev/sdk`'s `KnownSecretKeys` and `EnvironmentSecrets` interfaces
1151
+ 3. Writes the file to `.kici/types/secrets.d.ts`
1152
+
1153
+ After generating types, `ctx.secrets.get('MY_KEY')` and `ctx.secrets.expose('DB_HOST')` gain autocomplete and type checking in your IDE.
1154
+
1155
+ **Git workflow:** Commit the generated `.kici/types/secrets.d.ts` so team members get type checking without needing orchestrator access. Run `kici types` to refresh when environments change.
1156
+
1157
+ **Auto-regeneration:** `kici compile` automatically runs `kici types` after successful compilation when authenticated. See the [kici compile](https://docs.kici.dev/user/cli-reference/#kici-compile) section for details.
1158
+
1159
+ **Escape hatch:** For dynamic keys not in the generated types, use a cast: `(ctx.secrets as any).DYNAMIC_KEY`.
1160
+
1161
+ ### kici fixture
1162
+
1163
+ Generate a fixture template for an event type. Useful for creating custom test payloads.
1164
+
1165
+ ```bash
1166
+ kici fixture <event> [options]
1167
+ ```
1168
+
1169
+ **Arguments:**
1170
+
1171
+ | Argument | Required | Description |
1172
+ | -------- | -------- | ----------------------------- |
1173
+ | `event` | yes | Event to generate fixture for |
1174
+
1175
+ **Valid events:** `pr:open`, `pr:sync`, `pr:close`, `pr:reopen`, `push`, `tag`, `comment`, `review`, `review_comment`, `release`, `dispatch`, `create`, `delete`, `status`, `workflow_run`, `fork`, `star`, `watch`, `kici_event`, `workflow_complete`, `job_complete`, `generic_webhook`, `schedule`, `lifecycle` (many support `:action` suffixes, e.g. `comment:edited`, `release:published`, `lifecycle:workflow_complete`). `webhook:<source>` is a shorthand alias for `generic_webhook:<source>`.
1176
+
1177
+ **Options:**
1178
+
1179
+ | Option | Default | Description |
1180
+ | ----------------- | ------- | ------------------------------- |
1181
+ | `--output <path>` | stdout | Write to file instead of stdout |
1182
+
1183
+ **Examples:**
1184
+
1185
+ ```bash
1186
+ # Print fixture to stdout
1187
+ kici fixture pr:open
1188
+
1189
+ # Write fixture to file
1190
+ kici fixture pr:open --output fixtures/pr-open.json
1191
+
1192
+ # Generate push fixture
1193
+ kici fixture push --output fixtures/push.json
1194
+ ```
1195
+
1196
+ Use generated fixtures as reference when writing test fixture files in `.kici/tests/`:
1197
+
1198
+ ```bash
1199
+ kici fixture pr:open --output fixtures/pr-open-reference.json
1200
+ # Use the generated JSON as reference when writing .kici/tests/pr-open.ts
1201
+ ```
1202
+
1203
+ ### kici init
1204
+
1205
+ Initialize a `.kici/` directory with default workflow templates.
1206
+
1207
+ ```bash
1208
+ kici init [options]
1209
+ ```
1210
+
1211
+ **Options:**
1212
+
1213
+ | Option | Default | Description |
1214
+ | ------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------- |
1215
+ | `--force` | `false` | Overwrite existing `.kici/` directory |
1216
+ | `--skip-install` | `false` | Create files without installing dependencies |
1217
+ | `--package-manager <npm\|pnpm\|yarn>` | auto-detect | Force a package manager for the install step (default: detect from your repo) |
1218
+ | `--mjs` | `false` | JavaScript-only mode (no TypeScript, no deps) |
1219
+ | `--no-agents-md` | writes `AGENTS.md` | Skip writing `.kici/AGENTS.md` (the LLM authoring context file) |
1220
+ | `--private-registry <url>` | none | Scaffold a workflow `registries:` entry pointing at `<url>` (e.g. CodeArtifact, GH Packages, Verdaccio) |
1221
+ | `--private-registry-scope <scope>` | none | Optional npm package scope (e.g. `@my-org`) for the private registry |
1222
+ | `--private-registry-secret <ref>` | `production:NPM_TOKEN` | Qualified secret reference (`env:NAME`) the private registry token comes from |
1223
+
1224
+ **Examples:**
1225
+
1226
+ ```bash
1227
+ # Interactive initialization
1228
+ kici init
1229
+
1230
+ # Overwrite existing setup
1231
+ kici init --force
1232
+
1233
+ # Skip dependency install (faster, install manually later)
1234
+ kici init --skip-install
1235
+
1236
+ # Force a specific package manager (default: detect from your repo)
1237
+ kici init --package-manager pnpm
1238
+
1239
+ # JavaScript mode (no TypeScript)
1240
+ kici init --mjs
1241
+
1242
+ # Skip writing the AGENTS.md LLM authoring context file
1243
+ kici init --no-agents-md
1244
+
1245
+ # Scaffold a workflow registries entry for a private npm registry
1246
+ kici init --private-registry https://npm.pkg.github.com/ \
1247
+ --private-registry-scope @my-org \
1248
+ --private-registry-secret production:GITHUB_PACKAGES_TOKEN
1249
+ ```
1250
+
1251
+ **What it creates:**
1252
+
1253
+ ```
1254
+ .kici/
1255
+ workflows/
1256
+ hello-world.ts # Minimal push workflow
1257
+ pr-checks.ts # Comprehensive PR workflow
1258
+ tests/
1259
+ push-test.ts # Sample test fixture
1260
+ types/ # Directory for generated type declarations (kici types)
1261
+ package.json # Dependencies (@kici-dev/sdk)
1262
+ tsconfig.json # TypeScript configuration (includes types/**/*.d.ts)
1263
+ .kiciignore # Default exclusion patterns for test uploads
1264
+ ```
1265
+
1266
+ In interactive mode (TTY), `kici init` prompts you to:
1267
+
1268
+ 1. Select which workflow templates to include
1269
+ 2. Optionally install a pre-commit hook
1270
+
1271
+ **Package manager:** the dependency install step uses the package manager detected for your repo — the `packageManager` field in the nearest `package.json` (Corepack convention), then a lockfile in the project root (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm), then the package manager that invoked `kici` (`pnpm dlx` / `yarn dlx` / `npx`), defaulting to npm. Pass `--package-manager <npm|pnpm|yarn>` to override detection, or `--skip-install` to set up the files and install later yourself.
1272
+
1273
+ **Development mode:** When `KICI_DEV=true` or `package.json` has `"kici": { "development": true }`, the generated `package.json` uses prerelease-compatible version ranges (`>=0.0.1-0`) so npm resolves Verdaccio's prerelease builds.
1274
+
1275
+ ### kici hook install
1276
+
1277
+ Install a pre-commit hook that runs `kici compile` before each commit.
1278
+
1279
+ ```bash
1280
+ kici hook install [options]
1281
+ ```
1282
+
1283
+ **Options:**
1284
+
1285
+ | Option | Default | Description |
1286
+ | ------- | ------- | ------------------------------------------ |
1287
+ | `--git` | `false` | Use raw git hook (`.git/hooks/pre-commit`) |
1288
+
1289
+ **Examples:**
1290
+
1291
+ ```bash
1292
+ # Auto-detect hook tool (husky, lint-staged, etc.)
1293
+ kici hook install
1294
+
1295
+ # Force raw git hook
1296
+ kici hook install --git
1297
+ ```
1298
+
1299
+ The command auto-detects existing hook tools in your project:
1300
+
1301
+ - **Husky**: Adds to `.husky/pre-commit`
1302
+ - **lint-staged**: Adds to lint-staged configuration
1303
+ - **Raw git**: Writes `.git/hooks/pre-commit`
1304
+
1305
+ If multiple tools are detected, you are prompted to choose.
1306
+
1307
+ ### kici endpoints
1308
+
1309
+ List all webhook entrypoints for the current project. Reads the compiled lock file and displays webhook URLs grouped by type (git provider, generic webhooks, scheduled, event-driven).
1310
+
1311
+ ```bash
1312
+ kici endpoints [options]
1313
+ ```
1314
+
1315
+ **Options:**
1316
+
1317
+ | Option | Default | Description |
1318
+ | ------------------- | ------- | ----------------------- |
1319
+ | `--kici-dir <path>` | `.kici` | Path to .kici directory |
1320
+
1321
+ **Prerequisites:** Run `kici compile` first to generate the lock file.
1322
+
1323
+ **Examples:**
1324
+
1325
+ ```bash
1326
+ # List all webhook entrypoints
1327
+ kici endpoints
1328
+
1329
+ # Custom .kici directory
1330
+ kici endpoints --kici-dir packages/app/.kici
1331
+ ```
1332
+
1333
+ ### kici workflows list
1334
+
1335
+ List permanently registered workflows on the orchestrator.
1336
+
1337
+ ```bash
1338
+ kici workflows list [options]
1339
+ ```
1340
+
1341
+ **Options:**
1342
+
1343
+ | Option | Default | Description |
1344
+ | ----------------------- | ------- | ---------------------------------------------- |
1345
+ | `--json` | `false` | Output as JSON |
1346
+ | `--stale <duration>` | none | Filter stale registrations (e.g., `30d`, `7d`) |
1347
+ | `--trigger-type <type>` | none | Filter by trigger type |
1348
+ | `--repo <repo>` | none | Filter by repository |
1349
+
1350
+ **Examples:**
1351
+
1352
+ ```bash
1353
+ # List all registered workflows
1354
+ kici workflows list
1355
+
1356
+ # JSON output for scripting
1357
+ kici workflows list --json
1358
+
1359
+ # Show workflows not updated in 30 days
1360
+ kici workflows list --stale 30d
1361
+
1362
+ # Filter by trigger type
1363
+ kici workflows list --trigger-type push
1364
+
1365
+ # Filter by repository
1366
+ kici workflows list --repo my-org/my-repo
1367
+ ```
1368
+
1369
+ ### kici docs
1370
+
1371
+ Open the KiCI documentation site in the default browser. With the `llm` subcommand, print the LLM-friendly documentation bundle that ships with `@kici-dev/compiler` — pipe it into a coding agent's context buffer to brief the agent on authoring conventions without an internet round-trip.
1372
+
1373
+ ```bash
1374
+ kici docs # open https://kici.dev/docs/
1375
+ kici docs --no-open # print the URL instead of opening a browser
1376
+ kici docs llm # print the llms.txt index (a router over the task bundles)
1377
+ kici docs llm sdk # print the SDK task bundle
1378
+ kici docs llm full # print llms-full.txt (every page in one file)
1379
+ kici docs llm sdk --out sdk-context.md # write a bundle to a file
1380
+ ```
1381
+
1382
+ **Examples:**
1383
+
1384
+ ```bash
1385
+ # Open the docs site in your browser
1386
+ kici docs
1387
+
1388
+ # Pipe just the SDK bundle into a coding agent (small, task-scoped context)
1389
+ kici docs llm sdk | claude -- "Read this and help me author a deploy workflow"
1390
+
1391
+ # Save the router index for offline reference
1392
+ kici docs llm --out kici-llms-index.txt
1393
+ ```
1394
+
1395
+ Bundles are regenerated from `docs/` every time `@kici-dev/compiler` is built, so they always match your installed CLI version. The index lists each task bundle — `getting-started`, `sdk`, `cli`, `patterns`, `features`, `providers`, `architecture` — with its size and a one-line purpose; pass the bundle id as the topic. Every cross-reference link inside a bundle is an absolute `docs.kici.dev` URL. The same files are published online following the [llms.txt convention](https://llmstxt.org/).
1396
+
1397
+ ### kici admin
1398
+
1399
+ Operator-facing commands for running instances.
1400
+
1401
+ #### kici admin drain-worker
1402
+
1403
+ Trigger graceful drain on a worker instance. Sends a POST request to the worker's `/drain` endpoint.
1404
+
1405
+ ```bash
1406
+ kici admin drain-worker [options]
1407
+ ```
1408
+
1409
+ **Options:**
1410
+
1411
+ | Option | Required | Description |
1412
+ | ------------- | -------- | --------------------------------------------- |
1413
+ | `--url <url>` | yes | Worker URL (e.g., `http://worker-host:10143`) |
1414
+
1415
+ **Examples:**
1416
+
1417
+ ```bash
1418
+ # Drain a local worker
1419
+ kici admin drain-worker --url http://localhost:10143
1420
+
1421
+ # Drain a remote worker
1422
+ kici admin drain-worker --url http://worker-2.internal:10143
1423
+ ```
1424
+
1425
+ **Exit codes:**
1426
+
1427
+ | Code | Meaning |
1428
+ | ---- | ----------------------------------- |
1429
+ | 0 | Drain request accepted |
1430
+ | 1 | Error (unreachable or request fail) |
1431
+
1432
+ ### kici verify-attestation
1433
+
1434
+ Verify a KiCI build-provenance attestation bundle offline. A bundle is the signed package a workflow step produces via `ctx.attestProvenance(...)`: a DSSE-wrapped SLSA in-toto statement, the ephemeral public key that signed it, and the KiCI identity token that anchors the build context. For the end-to-end attest → verify → view journey, see the [build provenance guide](https://docs.kici.dev/user/provenance/). Verification establishes the full chain — the identity token verifies against the trusted issuer's JWKS, the DSSE signature verifies against the bundled key, and the statement's build context must match the token's claims (a mismatch is a hard failure). When an `[artifact]` is given, its SHA-256 digest is also matched against the attestation subject.
1435
+
1436
+ ```bash
1437
+ kici verify-attestation [artifact] --bundle <path-or-url> --trust-root <url-or-file> [options]
1438
+ ```
1439
+
1440
+ **Arguments:**
1441
+
1442
+ | Argument | Required | Description |
1443
+ | ------------ | -------- | --------------------------------------------------------------------- |
1444
+ | `[artifact]` | no | Artifact path to digest-check against the attestation subject digest. |
1445
+
1446
+ **Options:**
1447
+
1448
+ | Option | Required | Description |
1449
+ | ---------------------------- | -------- | ----------------------------------------------------------------------------------------- |
1450
+ | `--bundle <path-or-url>` | yes | Path or `http(s)` URL to the attestation bundle JSON. |
1451
+ | `--trust-root <url-or-file>` | yes | Trusted issuer (see below). The token issuer is pinned to it, never taken from the token. |
1452
+ | `--audience <aud>` | no | Expected token audience (defaults to the KiCI provenance audience). |
1453
+ | `--json` | no | Print the structured verification result as JSON instead of human-readable output. |
1454
+
1455
+ **Trust root:** the verifier never trusts the issuer named inside the token — you supply the trusted issuer out-of-band via `--trust-root`, in one of two forms:
1456
+
1457
+ - **Online — an HTTPS issuer URL.** The verifier fetches `<url>/.well-known/openid-configuration`, reads its `issuer` and `jwks_uri`, and fetches the JWKS. The token's `iss` is pinned to the discovery document's `issuer`.
1458
+ - **Offline — a self-contained trust-root file.** A local JSON file with the issuer and JWKS inlined, so no network access is needed (air-gapped verification):
1459
+
1460
+ ```json
1461
+ {
1462
+ "issuer": "https://platform.example/issuer",
1463
+ "jwks": {
1464
+ "keys": [
1465
+ { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "alg": "ES256", "kid": "..." }
1466
+ ]
1467
+ }
1468
+ }
1469
+ ```
1470
+
1471
+ **Examples:**
1472
+
1473
+ ```bash
1474
+ # Online: verify a bundle against a deployed issuer, digest-checking the artifact
1475
+ kici verify-attestation ./dist/app.tgz \
1476
+ --bundle ./app.tgz.kici.json \
1477
+ --trust-root https://platform.example/issuer
1478
+
1479
+ # Offline / air-gapped: verify against a self-contained trust-root file
1480
+ kici verify-attestation ./dist/app.tgz \
1481
+ --bundle ./app.tgz.kici.json \
1482
+ --trust-root ./kici-trust-root.json
1483
+
1484
+ # Machine-readable result for scripting
1485
+ kici verify-attestation --bundle ./app.tgz.kici.json \
1486
+ --trust-root https://platform.example/issuer --json
1487
+ ```
1488
+
1489
+ **Exit codes:**
1490
+
1491
+ | Code | Meaning |
1492
+ | ---- | ----------------------------------------------------------------------------------- |
1493
+ | 0 | Verified — signature, identity, build context (and digest, if checked) all pass |
1494
+ | 1 | Not verified, or an error (missing flag, unreadable bundle, unreachable trust root) |
1495
+
1496
+ ## Workflow discovery
1497
+
1498
+ The CLI discovers workflows by scanning `.kici/workflows/*.ts` (or `.mjs` in MJS mode). Each file should `export default` a single workflow:
1499
+
1500
+ ```typescript
1501
+ // .kici/workflows/ci.ts
1502
+ import { workflow, job, step, pr } from '@kici-dev/sdk';
1503
+
1504
+ export default workflow('ci', {
1505
+ on: pr(),
1506
+ jobs: [
1507
+ /* ... */
1508
+ ],
1509
+ });
1510
+ ```
1511
+
1512
+ Multiple workflow files are supported -- each becomes a separate workflow in `kici.lock.json`.
1513
+
1514
+ ## Lock file
1515
+
1516
+ The `kici compile` command produces `.kici/kici.lock.json` inside the `.kici` directory. This file:
1517
+
1518
+ - Contains all workflow definitions in a portable JSON format
1519
+ - Is used by the orchestrator to evaluate triggers without code checkout
1520
+ - Should be committed to version control
1521
+ - Is regenerated on every `kici compile` run
1522
+
1523
+ Use `kici compile --check` in CI to validate that workflows are correct without writing files. For the full story on drift, pre-commit/CI, and agent-side verification, see [Lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/).
1524
+
1525
+ ## Exit codes
1526
+
1527
+ All commands follow a consistent exit code convention:
1528
+
1529
+ | Code | Meaning |
1530
+ | ---- | -------------------- |
1531
+ | 0 | Success |
1532
+ | 1 | Failure (see output) |
1533
+
1534
+ ## Debug output
1535
+
1536
+ Use `--debug` (on `kici run local`, `kici run remote`, `kici test`) or `--verbose` (on `kici compile`) for detailed output:
1537
+
1538
+ ```bash
1539
+ # Shows trigger matching, rule evaluation, decision traces
1540
+ kici run local push --debug
1541
+
1542
+ # Shows detailed compilation steps
1543
+ kici compile --verbose
1544
+
1545
+ # Shows trigger matching preview
1546
+ kici test pr:open --debug
1547
+ ```
1548
+
1549
+ Set `KICI_DEBUG=true` for additional internal debug output across all commands.
1550
+
1551
+ ## Environment variables
1552
+
1553
+ | Variable | Description |
1554
+ | ------------ | ----------------------------------------- |
1555
+ | `KICI_DEV` | Set to `true` for development mode |
1556
+ | `KICI_DEBUG` | Set to `true` for verbose internal output |
1557
+ | `CI` | When `true`, disables interactive prompts |
1558
+
1559
+ ## See also
1560
+
1561
+ - [Getting started](https://docs.kici.dev/user/getting-started/) -- install the SDK and write your first workflow
1562
+ - [Testing guide](https://docs.kici.dev/user/testing-guide/) -- writing fixtures, remote test runs, secret contexts, and repo state transfer
1563
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) -- complete API for the workflow definitions that the CLI compiles
1564
+ - [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/) -- example workflows to compile and test with these commands
1565
+
1566
+ ---
1567
+
1568
+ ## Lifecycle hooks
1569
+
1570
+ Source: https://docs.kici.dev/user/hooks/
1571
+
1572
+ Hooks are callbacks that run at specific points in the execution lifecycle. They let you react to outcomes (cancellation, success, failure) and perform cleanup without affecting the execution flow.
1573
+
1574
+ ## Hook types
1575
+
1576
+ KiCI supports six hook types at three levels (step, job, workflow):
1577
+
1578
+ | Hook | When it runs | Available on |
1579
+ | ------------ | ------------------------------------ | ------------------- |
1580
+ | `onCancel` | After step/job/workflow is cancelled | Step, Job, Workflow |
1581
+ | `cleanup` | Always (success, failure, or cancel) | Step, Job, Workflow |
1582
+ | `onSuccess` | After job/workflow succeeds | Job, Workflow |
1583
+ | `onFailure` | After job/workflow fails | Job, Workflow |
1584
+ | `beforeStep` | Before each step in a job | Job |
1585
+ | `afterStep` | After each step in a job | Job |
1586
+
1587
+ ## Basic usage
1588
+
1589
+ ### Job-level hooks
1590
+
1591
+ ```typescript
1592
+ import { workflow, job, step, push } from '@kici-dev/sdk';
1593
+
1594
+ export default workflow('deploy', {
1595
+ on: push({ branches: ['main'] }),
1596
+ jobs: [
1597
+ job('deploy-prod', {
1598
+ runsOn: 'linux',
1599
+ steps: [
1600
+ step('deploy', async ({ $ }) => {
1601
+ await $`kubectl apply -f manifests/`;
1602
+ }),
1603
+ ],
1604
+ onCancel: async (ctx) => {
1605
+ console.log(`Deploy cancelled: ${ctx.outcome.reason}`);
1606
+ await ctx.$`kubectl rollout undo deployment/app`;
1607
+ },
1608
+ cleanup: async (ctx) => {
1609
+ // Always runs -- release lock, notify team, etc.
1610
+ await ctx.$`curl -X POST https://slack.com/webhook -d '{"text": "Deploy ${ctx.outcome.status}"}'`;
1611
+ },
1612
+ onSuccess: async (ctx) => {
1613
+ console.log(`Deploy succeeded in ${ctx.outcome.duration}ms`);
1614
+ },
1615
+ onFailure: async (ctx) => {
1616
+ console.log(`Deploy failed at step: ${ctx.outcome.failedStep}`);
1617
+ },
1618
+ gracePeriod: 60, // 60 seconds before SIGKILL on cancel
1619
+ }),
1620
+ ],
1621
+ });
1622
+ ```
1623
+
1624
+ ### Step-level hooks
1625
+
1626
+ ```typescript
1627
+ step('download-artifacts', {
1628
+ run: async ({ $ }) => {
1629
+ await $`wget https://artifacts.example.com/build.tar.gz`;
1630
+ },
1631
+ onCancel: async (ctx) => {
1632
+ // Clean up partial downloads
1633
+ await ctx.$`rm -f build.tar.gz`;
1634
+ },
1635
+ cleanup: async (ctx) => {
1636
+ await ctx.$`rm -rf /tmp/staging`;
1637
+ },
1638
+ });
1639
+ ```
1640
+
1641
+ ### Workflow-level hooks
1642
+
1643
+ ```typescript
1644
+ workflow('ci', {
1645
+ on: push({ branches: ['main'] }),
1646
+ jobs: [
1647
+ /* ... */
1648
+ ],
1649
+ onCancel: async (ctx) => {
1650
+ // Notify when any job in the workflow is cancelled
1651
+ console.log('CI workflow cancelled');
1652
+ },
1653
+ cleanup: async (ctx) => {
1654
+ // Always runs after all jobs complete
1655
+ console.log(`CI workflow finished with status: ${ctx.outcome.status}`);
1656
+ },
1657
+ });
1658
+ ```
1659
+
1660
+ ## Hook context
1661
+
1662
+ Hook functions receive the same `StepContext` as regular steps (`$`, `ctx`, `log`, `env`), plus an `outcome` object with metadata about the execution result.
1663
+
1664
+ ### ctx.outcome
1665
+
1666
+ ```typescript
1667
+ interface OutcomeMetadata {
1668
+ /** Final status of the job/workflow. */
1669
+ status: 'cancelled' | 'success' | 'failed';
1670
+ /** Reason for cancellation (e.g., "User requested", "Superseded by run #42"). */
1671
+ reason?: string;
1672
+ /** Name of the step that caused failure (for onFailure hooks). */
1673
+ failedStep?: string;
1674
+ /** Outputs from all completed steps. */
1675
+ stepOutputs: Record<string, unknown>;
1676
+ /** Total execution duration in milliseconds. */
1677
+ duration: number;
1678
+ }
1679
+ ```
1680
+
1681
+ ### Capabilities
1682
+
1683
+ Hooks can do everything regular steps can:
1684
+
1685
+ - Run shell commands via `$`
1686
+ - Set environment variables via `ctx.setEnv()` and prepend to `PATH` via `ctx.addPath()`
1687
+ - Access previous step outputs via `ctx.outputsOf()` and `ctx.jobOutputs()`
1688
+ - Publish encrypted secret outputs via `ctx.setSecretOutput()`
1689
+ - Log via `log.info()`, `log.error()`, etc.
1690
+
1691
+ ## Hook timeout
1692
+
1693
+ Each hook has a timeout (default: 5 minutes). You can customize it per-hook:
1694
+
1695
+ ```typescript
1696
+ job('deploy', {
1697
+ runsOn: 'linux',
1698
+ steps: [
1699
+ /* ... */
1700
+ ],
1701
+ cleanup: {
1702
+ run: async (ctx) => {
1703
+ await ctx.$`./lengthy-cleanup.sh`;
1704
+ },
1705
+ timeout: 10 * 60 * 1000, // 10 minutes in ms
1706
+ },
1707
+ });
1708
+ ```
1709
+
1710
+ ## Hook execution order
1711
+
1712
+ Hooks execute inside-out on cancellation (like stack unwinding):
1713
+
1714
+ 1. **Step-level** cleanup (on the cancelled step)
1715
+ 2. **Job-level** onCancel, then cleanup
1716
+ 3. **Workflow-level** onCancel, then cleanup
1717
+
1718
+ On success: step afterStep (after each step), then job onSuccess + cleanup, then workflow onSuccess + cleanup.
1719
+
1720
+ On failure: job onFailure + cleanup, then workflow onFailure + cleanup.
1721
+
1722
+ **cleanup always runs** -- regardless of whether the outcome was success, failure, or cancel.
1723
+
1724
+ ## Hooks are observers
1725
+
1726
+ Hooks follow the "one mechanism per concern" principle:
1727
+
1728
+ - **Rules** control whether a step/job executes (conditional logic)
1729
+ - **Hooks** react to execution outcomes (lifecycle callbacks)
1730
+
1731
+ Hooks cannot short-circuit step execution or change the execution flow. They observe and respond.
1732
+
1733
+ ## beforeStep and afterStep
1734
+
1735
+ These job-level hooks run around every step in the job:
1736
+
1737
+ ```typescript
1738
+ job('test', {
1739
+ runsOn: 'linux',
1740
+ beforeStep: async (ctx) => {
1741
+ console.log(`Starting step at ${new Date().toISOString()}`);
1742
+ },
1743
+ afterStep: async (ctx) => {
1744
+ console.log(`Step completed with status: ${ctx.outcome.status}`);
1745
+ },
1746
+ steps: [
1747
+ step('lint', async ({ $ }) => {
1748
+ await $`pnpm lint`;
1749
+ }),
1750
+ step('test', async ({ $ }) => {
1751
+ await $`pnpm test`;
1752
+ }),
1753
+ ],
1754
+ });
1755
+ ```
1756
+
1757
+ `afterStep` runs immediately after its step, before the next step starts (not deferred to the end of the job).
1758
+
1759
+ ## Step-level rules
1760
+
1761
+ Step-level rules control whether a step executes, evaluated at runtime by the agent:
1762
+
1763
+ ```typescript
1764
+ import { step, rule, skip, isEventType } from '@kici-dev/sdk';
1765
+
1766
+ step('deploy', {
1767
+ run: async ({ $ }) => {
1768
+ await $`kubectl apply -f manifests/`;
1769
+ },
1770
+ rules: [
1771
+ rule('only on main pushes', (ctx) => {
1772
+ if (!isEventType(ctx.event, 'push')) return false;
1773
+ return ctx.event.payload.ref === 'refs/heads/main';
1774
+ }),
1775
+ ],
1776
+ });
1777
+
1778
+ // Or use skip() for explicit skip with a reason
1779
+ step('optional-check', {
1780
+ run: async ({ $ }) => {
1781
+ await $`./optional-check.sh`;
1782
+ },
1783
+ rules: [skip('not needed in CI', () => true)],
1784
+ });
1785
+ ```
1786
+
1787
+ When a rule returns `false`, the step is reported as `skipped` and subsequent steps continue normally. Skipped steps don't cause the job to fail.
1788
+
1789
+ Step rules have access to runtime context via `RuleContext`: `event` (typed discriminated union), `changedFiles`, `env`, and `$`. They evaluate agent-side (unlike job-level rules which evaluate at the orchestrator during trigger matching).
1790
+
1791
+ ## Hook failure behavior
1792
+
1793
+ If a hook throws an error or times out:
1794
+
1795
+ - The job status changes to `failed` with a compound reason (e.g., "cancelled (onCancel hook failed: Connection timeout)")
1796
+ - Remaining hooks for that level are skipped
1797
+ - The failure is visible in the dashboard as a failed hook step
1798
+ - Force cancel kills running hooks immediately via SIGKILL
1799
+
1800
+ This behavior is consistent across all hook types.
1801
+
1802
+ ---
1803
+
1804
+ _Source: `packages/sdk/src/hooks/`, `packages/sdk/src/types.ts`_
1805
+
1806
+ ---
1807
+
1808
+ ## Lock file and workflow drift
1809
+
1810
+ Source: https://docs.kici.dev/user/lock-file-and-drift/
1811
+
1812
+ KiCI uses a **two-artifact model**: TypeScript workflows are the source of truth; the lock file (`kici.lock.json`) is the execution contract. The orchestrator reads only the lock file to match triggers and decide cache vs build. Keeping these in sync is important.
1813
+
1814
+ ## Why the lock file matters
1815
+
1816
+ - **Orchestrator** fetches the lock file at the commit SHA and uses it to evaluate triggers and to look up the cached `.kici/` source tarball + `node_modules` tarball. It never runs your TypeScript.
1817
+ - **Agents** download the cached source tarball (or, on cold cache, the build agent clones + packs it), register the shared TypeScript loader hook, and dynamic-`import()` the workflow `.ts` directly. The lock file's per-workflow `contentHash` identifies the expected raw-source bytes and is verified against the extracted source before any step runs.
1818
+
1819
+ If you change a workflow file (`.ts`) but do **not** regenerate and commit the lock file, the repo at that commit has **drift**: the lock file no longer matches the source. Triggers and cache keys can be wrong, and runs can fail with a clear “stale lock file” error once the agent verifies the hash.
1820
+
1821
+ ## Lock file structure
1822
+
1823
+ The lock file (`kici.lock.json`) is a JSON file with the following top-level fields:
1824
+
1825
+ | Field | Description |
1826
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1827
+ | `schemaVersion` | Lock file schema version (currently 20). Incremented on breaking format changes. |
1828
+ | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
1829
+ | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
1830
+ | `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
1831
+ | `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
1832
+
1833
+ Each workflow entry includes:
1834
+
1835
+ | Field | Description |
1836
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1837
+ | `name` | Workflow name. |
1838
+ | `source` | Per-workflow source file and export reference. |
1839
+ | `contentHash` | SHA-256 of the raw workflow source mixed with `compileSchemaVersion` (and an `assetDigest` of declared `hashFiles` when present): `SHA-256(compileSchemaVersion + ":" + rawSource [+ "\0" + assetDigest])`. The orchestrator uses this as the source-tarball cache key and the agent re-computes it against the extracted source to detect drift. |
1840
+ | `compileSchemaVersion` | Compiler schema version used when computing `contentHash` (currently `5`). The hash input is line-ending-normalized (CRLF → LF) so a lock file produced on Linux matches the agent's hash on Windows where Git's `core.autocrlf=true` rewrites checked-out text to CRLF. Bumping the schema version invalidates every existing source cache entry even if source is unchanged, which is the correct behavior when the compile-time or runtime contract changes. |
1841
+ | `triggers` | Trigger definitions extracted from the workflow (used by the orchestrator for event matching). |
1842
+ | `jobs` | Job definitions with scheduling metadata (runsOn, needs, matrix, environment, concurrency, container, checkout, gracePeriod, label routing, dynamic fields, etc.). |
1843
+ | `rules` | Workflow-level conditional rules (optional). Stored as dynamic references since rule functions cannot be serialized. |
1844
+ | `description` | Optional workflow description. |
1845
+ | `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](https://docs.kici.dev/user/lock-file-and-drift/#extra-files-in-the-content-hash-hashfiles). |
1846
+ | `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
1847
+ | `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
1848
+ | `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](https://docs.kici.dev/user/private-registries/). |
1849
+ | `installEnv` | Extra qualified secret refs (`<environment>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](https://docs.kici.dev/user/private-registries/). |
1850
+ | `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
1851
+ | `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
1852
+ | Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
1853
+
1854
+ ## Rule: commit both together
1855
+
1856
+ **Always commit `.kici/kici.lock.json` in the same commit as the workflow source files it was generated from.**
1857
+
1858
+ 1. After editing `.kici/workflows/*.ts`, run:
1859
+ ```bash
1860
+ npx kici compile
1861
+ ```
1862
+ 2. Stage both the workflow file(s) and `.kici/kici.lock.json`.
1863
+ 3. Commit them together.
1864
+
1865
+ That way the lock file at every commit SHA matches the workflow source at that SHA.
1866
+
1867
+ ## Catch drift early: pre-commit and CI
1868
+
1869
+ Use automation so drift is caught before it reaches the repo.
1870
+
1871
+ ### Pre-commit hook
1872
+
1873
+ Install a hook that compiles and stages the lock file before each commit:
1874
+
1875
+ ```bash
1876
+ npx kici hook install
1877
+ ```
1878
+
1879
+ This runs `kici compile && git add .kici/kici.lock.json` before each commit: if compilation fails the commit is blocked; if it succeeds the updated lock file is automatically staged. See [CLI Reference — kici hook](https://docs.kici.dev/user/cli-reference/#kici-hook) for options (husky, lefthook, pre-commit, prek, raw git).
1880
+
1881
+ ### CI check
1882
+
1883
+ In your CI pipeline, verify that the workflow source compiles without errors:
1884
+
1885
+ ```bash
1886
+ kici compile --check
1887
+ ```
1888
+
1889
+ This validates all workflows and generates the lock file in memory without writing it. If any workflow has syntax errors or invalid configuration, the command exits non-zero. Pair this with the agent-side hash verification (below) for full drift detection -- `--check` catches broken source, while the agent catches source-lock-file mismatches at run time.
1890
+
1891
+ ## Extra files in the content hash (`hashFiles`)
1892
+
1893
+ By default, the per-workflow content hash is `SHA-256(compileSchemaVersion + ":" + rawSource)` where `rawSource` is the TypeScript text of the workflow entry file. If your workflow depends on files outside `.kici/workflows/` -- configuration files, scripts, Dockerfiles, etc. -- changes to those files will **not** invalidate the cache unless you declare them.
1894
+
1895
+ Use the `hashFiles` option on a workflow to include additional paths or glob patterns (relative to the repo root) in the content hash:
1896
+
1897
+ ```typescript
1898
+ export default workflow('deploy', {
1899
+ hashFiles: ['config.json', 'scripts/*.sh'],
1900
+ jobs: [
1901
+ /* ... */
1902
+ ],
1903
+ });
1904
+ ```
1905
+
1906
+ When any of the matched files change, the content hash formula becomes `SHA-256(compileSchemaVersion + ":" + rawSource + "\0" + assetDigest)` where `assetDigest` is a deterministic encoding of the resolved file paths and their contents. This busts the source-tarball cache and forces the build agent to pack and upload a fresh `source/{contentHash}.tar.gz`. The resolved file paths are recorded in the lock file under `resolvedHashFiles` so the agent can verify without re-discovering the workflow.
1907
+
1908
+ ## Agent-side safety net
1909
+
1910
+ If drift still occurs (e.g. someone committed only the `.ts` change), the agent detects it at run time before any step runs:
1911
+
1912
+ - After extracting the `.kici/` source tarball (or loading source from a `git clone` on the build path), the agent reads the workflow entry file and re-computes `contentHash = SHA-256(compileSchemaVersion + ":" + rawSource [+ "\0" + assetDigest])` using the same formula as the compiler.
1913
+ - If the orchestrator sent a `contentHash` (from the lock file) and the computed hash does **not** match, the agent fails the run with an error like: **lock file is out of date** (workflow source changed without regenerating the lock file). The error includes the baked agent `@kici-dev/sdk` version + bundle hash so operators can debug cross-host compile mismatches.
1914
+
1915
+ So even without a pre-commit or CI check, a stale lock file will cause the run to fail with a clear message instead of running with the wrong workflow.
1916
+
1917
+ ## Summary
1918
+
1919
+ | Goal | What to do |
1920
+ | ---------------------------- | ------------------------------------------------------------------------------------------ |
1921
+ | Keep lock file in sync | Commit `kici.lock.json` with the workflow `.ts` changes; run `kici compile` before commit. |
1922
+ | Catch drift before commit | Install a pre-commit hook with `kici hook install`. |
1923
+ | Catch broken source in CI | Run `kici compile --check` in CI. |
1924
+ | Bust cache on external files | Add `hashFiles: [‘config.json’]` to include non-workflow files in the content hash. |
1925
+ | Fail fast when drift remains | Rely on the agent’s hash verification when it compiles from source. |
1926
+
1927
+ ## See also
1928
+
1929
+ - [Getting Started](https://docs.kici.dev/user/getting-started/) — compile and commit the lock file
1930
+ - [CLI Reference](https://docs.kici.dev/user/cli-reference/) — `kici compile`, `kici compile --check`, `kici hook`
1931
+ - [Architecture — Data flows](https://docs.kici.dev/architecture/data-flows/) — how the lock file is used in the pipeline
1932
+
1933
+ ---
1934
+
1935
+ ## Testing guide
1936
+
1937
+ Source: https://docs.kici.dev/user/testing-guide/
1938
+
1939
+ Test your workflows remotely against the full CI pipeline from your local machine. `kici run remote` uploads your current repo state (including uncommitted changes), triggers the pipeline, and streams execution logs back in real time.
1940
+
1941
+ ## Overview
1942
+
1943
+ `kici run remote` connects your local development environment to the remote orchestrator/agent pipeline. Instead of pushing a commit and waiting for CI, you can:
1944
+
1945
+ - Run any workflow against your current working tree (including unstaged changes)
1946
+ - Get real-time log output streamed back to your terminal
1947
+ - Give test runs test-scoped secrets — your local secret files and `--env` values (uploaded encrypted) plus any environment flagged `allowLocalExecution: true` — while production environments stay unreachable
1948
+ - Detect test mode in workflow code via `ctx.isTestRun`
1949
+
1950
+ The command is remote-only -- all execution happens on the orchestrator and agent. For local-only trigger matching previews, use `kici test <event>`.
1951
+
1952
+ :::note[Orchestrator prerequisite: cache storage]
1953
+ `kici run remote` uploads your working-tree overlay to the orchestrator's **cache storage** via a pre-signed URL, and the agent fetches it from there (see [Repo state transfer](https://docs.kici.dev/user/testing-guide/#repo-state-transfer)). The target orchestrator must therefore have cache storage enabled (`KICI_STORAGE_TYPE` = `s3` or `filesystem`).
1954
+
1955
+ - **Both quickstarts wire this up for you** — the [Docker / Podman quickstart](https://docs.kici.dev/user/quickstart/compose/) and the [bare-metal quickstart](https://docs.kici.dev/user/quickstart/bare-metal/) each ship a SeaweedFS object store and pre-fill the orchestrator's `KICI_STORAGE_*` block, so `kici run remote` works out of the box (see each guide's "run a workflow without pushing" step).
1956
+ - **A hand-rolled orchestrator deploy does not configure storage by default** — enable a backend before using `kici run remote`:
1957
+ - **`filesystem`** — simplest for a single-host orchestrator: set `KICI_STORAGE_TYPE=filesystem` and `KICI_STORAGE_FS_PATH=/var/lib/kici/cache`. No external service needed; blobs are served through the orchestrator's own HMAC-signed HTTP route.
1958
+ - **`s3`** — any S3-compatible bucket. **A non-public / self-hosted endpoint works**: set `KICI_STORAGE_TYPE=s3`, `KICI_STORAGE_BUCKET`, `KICI_STORAGE_ENDPOINT=https://your-endpoint` and (for most self-hosted services) `KICI_STORAGE_FORCE_PATH_STYLE=true`. If the developer machine running `kici run remote` reaches the bucket at a different address than the orchestrator, set `KICI_STORAGE_UPLOAD_ENDPOINT` to the developer-reachable address; if agents reach it at yet another address (e.g. agents in containers), set `KICI_STORAGE_EXTERNAL_ENDPOINT` to the agent-routable URL.
1959
+
1960
+ See [Storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/) for the full env-var reference.
1961
+ :::
1962
+
1963
+ ## Getting started
1964
+
1965
+ ### 1. Authenticate
1966
+
1967
+ ```bash
1968
+ kici login
1969
+ ```
1970
+
1971
+ This opens your browser for OAuth authentication and stores a personal access token in `~/.kici/config`. For CI/CD pipelines or headless environments, use `kici login --token <your-api-key>` or `kici login --device` instead. See [CLI authentication](https://docs.kici.dev/user/cli-auth/) for details.
1972
+
1973
+ ### 2. Write a test fixture
1974
+
1975
+ Fixtures define the events you want to simulate. They live in `.kici/tests/*.ts` and use the same SDK trigger functions as workflows.
1976
+
1977
+ ```typescript
1978
+ // .kici/tests/push-tests.ts
1979
+ import { fixture, push } from '@kici-dev/sdk';
1980
+
1981
+ export const pushMain = fixture('push-main', {
1982
+ event: push({ branches: ['main'] }),
1983
+ });
1984
+
1985
+ export const pushDevelop = fixture('push-develop', {
1986
+ event: push({ branches: ['develop'] }),
1987
+ });
1988
+ ```
1989
+
1990
+ Each file can export multiple fixtures. The `fixture()` factory takes an ID (used on the command line) and options including the event to simulate.
1991
+
1992
+ ### 3. Run a fixture
1993
+
1994
+ ```bash
1995
+ # List available fixtures
1996
+ kici run remote
1997
+
1998
+ # Run a specific fixture
1999
+ kici run remote push-main
2000
+
2001
+ # Run all fixtures matching a glob
2002
+ kici run remote 'push-*'
2003
+
2004
+ # Run everything
2005
+ kici run remote --all
2006
+ ```
2007
+
2008
+ The single quotes keep your shell from expanding `push-*` against local files, so the pattern reaches KiCI intact for its own fixture-glob matching.
2009
+
2010
+ ## Fixture reference
2011
+
2012
+ ### Event types
2013
+
2014
+ Fixtures accept any SDK trigger function as their event:
2015
+
2016
+ ```typescript
2017
+ import { fixture, push, pr, comment, tag, release } from '@kici-dev/sdk';
2018
+
2019
+ // Push event
2020
+ export const pushMain = fixture('push-main', {
2021
+ event: push({ branches: ['main'] }),
2022
+ });
2023
+
2024
+ // PR event
2025
+ export const prOpen = fixture('pr-open', {
2026
+ event: pr({ branches: ['main'], actions: ['opened'] }),
2027
+ });
2028
+
2029
+ // Comment event
2030
+ export const prComment = fixture('pr-comment', {
2031
+ event: comment({ actions: ['created'] }),
2032
+ });
2033
+
2034
+ // Tag event
2035
+ export const tagRelease = fixture('tag-release', {
2036
+ event: tag({ tags: ['v*'] }),
2037
+ });
2038
+
2039
+ // Release event
2040
+ export const published = fixture('release-published', {
2041
+ event: release({ actions: ['published'] }),
2042
+ });
2043
+ ```
2044
+
2045
+ ### Overrides
2046
+
2047
+ Override default payload values per fixture:
2048
+
2049
+ ```typescript
2050
+ export const pushFeature = fixture('push-feature', {
2051
+ event: push({ branches: ['feature/*'] }),
2052
+ branch: 'feature/auth', // Override branch name
2053
+ sha: 'abc123def456', // Override commit SHA
2054
+ repo: 'myorg/myrepo', // Override repository
2055
+ pr: 42, // Override PR number (for PR events)
2056
+ });
2057
+ ```
2058
+
2059
+ When not specified, these default to values detected from your local git repo (current branch, HEAD SHA, remote URL).
2060
+
2061
+ ### Secret context mappings
2062
+
2063
+ Map secret contexts to your fixture:
2064
+
2065
+ ```typescript
2066
+ export const pushWithSecrets = fixture('push-with-secrets', {
2067
+ event: push({ branches: ['main'] }),
2068
+ secrets: {
2069
+ db: 'test-database',
2070
+ api: 'test-api-keys',
2071
+ },
2072
+ });
2073
+ ```
2074
+
2075
+ This maps the `db` secret context to the `test-database` context, and `api` to `test-api-keys`.
2076
+
2077
+ This mapping is honored by **both** `kici run local` and `kici run remote`:
2078
+
2079
+ - For **`kici run local`** (see [`kici run local`](https://docs.kici.dev/user/cli-reference/#kici-run-local)), each named context is resolved from your local secret files (`.kici/.secrets`, `.env.local`, `secrets.yaml`, and `--env` flags).
2080
+ - For **`kici run remote`**, each named context maps to an orchestrator **environment**, and the orchestrator resolves that environment's secrets for the run. The target environment must be flagged `allowLocalExecution: true` — mapping a context to a missing or non-test environment rejects the run (see [Secret contexts for testing](https://docs.kici.dev/user/testing-guide/#secret-contexts-for-testing) below).
2081
+
2082
+ ### Async fixtures
2083
+
2084
+ For dynamic fixture configuration, export an async function:
2085
+
2086
+ ```typescript
2087
+ export const dynamicFixture = fixture('dynamic', async () => ({
2088
+ event: push({ branches: ['main'] }),
2089
+ sha: await getCurrentSha(),
2090
+ }));
2091
+ ```
2092
+
2093
+ ## Running tests
2094
+
2095
+ ### Basic commands
2096
+
2097
+ ```bash
2098
+ # List all available fixtures (discovers .kici/tests/*.ts)
2099
+ kici run remote
2100
+
2101
+ # Run a single fixture by ID
2102
+ kici run remote push-main
2103
+
2104
+ # Glob matching -- run all push-related fixtures
2105
+ kici run remote 'push-*'
2106
+
2107
+ # Run all fixtures sequentially
2108
+ kici run remote --all
2109
+
2110
+ # Run all fixtures in parallel
2111
+ kici run remote --all --parallel
2112
+ ```
2113
+
2114
+ ### Direct workflow run
2115
+
2116
+ Bypass trigger matching and run a specific workflow directly:
2117
+
2118
+ ```bash
2119
+ kici run remote --workflow ci
2120
+ ```
2121
+
2122
+ This skips the trigger evaluation step and runs all jobs in the named workflow.
2123
+
2124
+ ### Output modes
2125
+
2126
+ ```bash
2127
+ # Default: full log streaming with colored job prefixes
2128
+ kici run remote push-main
2129
+
2130
+ # Quiet: minimal output (just pass/fail result)
2131
+ kici run remote push-main --quiet
2132
+
2133
+ # JSON: machine-readable structured output
2134
+ kici run remote push-main --json
2135
+
2136
+ # JUnit XML: for CI integration
2137
+ kici run remote push-main --junit results.xml
2138
+ ```
2139
+
2140
+ ### Non-blocking execution
2141
+
2142
+ ```bash
2143
+ # Fire and forget -- returns immediately with run ID
2144
+ kici run remote push-main --no-wait
2145
+
2146
+ # Check status later
2147
+ kici runs show <run-id>
2148
+ ```
2149
+
2150
+ ### Cancellation
2151
+
2152
+ Press Ctrl+C during a running test to send a cancel signal to the orchestrator. The agent job will be terminated gracefully.
2153
+
2154
+ ## Repo state transfer
2155
+
2156
+ When you run `kici run remote`, the CLI:
2157
+
2158
+ 1. Detects all files differing from HEAD (staged, unstaged, and untracked)
2159
+ 2. Creates a compressed tarball of changed files
2160
+ 3. Encrypts the tarball using X25519 ECDH key exchange
2161
+ 4. Uploads the encrypted tarball to storage via a signed URL
2162
+ 5. Triggers the pipeline with a reference to the upload
2163
+
2164
+ The agent clones your repo at HEAD, then applies the overlay tarball on top -- giving you the exact same file state as your local working tree.
2165
+
2166
+ ### What gets included
2167
+
2168
+ - Modified tracked files (staged and unstaged)
2169
+ - New untracked files (not in `.gitignore`)
2170
+ - File deletions (tracked files you deleted locally)
2171
+
2172
+ ### What gets excluded
2173
+
2174
+ - Files matching `.gitignore` patterns
2175
+ - Files matching `.kiciignore` patterns (additional exclusions)
2176
+ - The `.git` directory itself
2177
+
2178
+ ### `.kiciignore`
2179
+
2180
+ Create a `.kiciignore` file in your repo root to exclude additional files from the upload:
2181
+
2182
+ ```
2183
+ # Large binaries
2184
+ *.bin
2185
+ *.iso
2186
+ data/fixtures/large-dataset.csv
2187
+
2188
+ # Local-only configs
2189
+ .env.local
2190
+ docker-compose.override.yml
2191
+ ```
2192
+
2193
+ The format is the same as `.gitignore` -- one glob pattern per line, `#` for comments.
2194
+
2195
+ ### Size limits
2196
+
2197
+ | Threshold | Behavior |
2198
+ | --------- | --------------------------------------------------------------------------------- |
2199
+ | < 50 MB | Normal upload |
2200
+ | 50-500 MB | Warning displayed, upload proceeds |
2201
+ | > 500 MB | Error -- reduce bundle size via `.kiciignore` or check for unintended large files |
2202
+
2203
+ The CLI always shows a pre-upload summary before transferring:
2204
+
2205
+ ```
2206
+ 12 files changed, 3 new, 1 deleted (2.3 MB compressed)
2207
+ ```
2208
+
2209
+ ## Secret contexts for testing
2210
+
2211
+ The goal of the test-secret model is to let test runs reach **test-only credentials** while keeping production credentials out of reach. `kici run remote` combines two sources of secrets for a test run, then merges them with a clear precedence and a fail-closed gate.
2212
+
2213
+ ### CLI-uploaded local secrets
2214
+
2215
+ `kici run remote` collects the same local secret values that `kici run local` reads — `.kici/.secrets`, `.kici/.env.local`, `.kici/secrets.yaml`, and any `--env KEY=VALUE` flags — and uploads them **encrypted** to the orchestrator alongside the run. The orchestrator decrypts them only to inject them into the agent for that run; the control plane never sees the values.
2216
+
2217
+ ```bash
2218
+ # Provide an ad-hoc test value for a single remote run
2219
+ kici run remote push-main --env KICI_DATABASE_URL=postgresql://localhost/test
2220
+ ```
2221
+
2222
+ `--env` provides a **flat** per-run override; `--context <ctx>.<KEY>=<value>` is its sibling for a **namespaced** per-run override, placing the value under the named context `ctx`. Both are uploaded **encrypted** and follow the same precedence rule below — a CLI-supplied value wins over the orchestrator test-environment secret on a key collision.
2223
+
2224
+ ```bash
2225
+ # Provide a namespaced per-run value under the 'db' context
2226
+ kici run remote push-db --context db.KICI_DATABASE_URL=postgresql://localhost/test
2227
+ ```
2228
+
2229
+ Because these values originate on your machine, they are the natural place to put throwaway test credentials without touching any orchestrator-stored secret.
2230
+
2231
+ ### Orchestrator test-environment secrets
2232
+
2233
+ In addition to your uploaded values, the orchestrator resolves test-scoped secrets from its own store for a remote test run:
2234
+
2235
+ - The job's own declared `environment` contributes its resolved secrets (flat). Static strings and **pure dynamic functions** both participate: a pure `environment:` function (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is evaluated against the fixture's simulated event, and the resolved name is gated and resolved like a static one. Impure dynamic functions (those requiring an init job) are not evaluated for test runs — use a fixture `secrets:` mapping (or `--context`) to supply such a job's secrets.
2236
+ - Each fixture `secrets: { ctx: envName }` mapping resolves the named environment's secrets under the namespaced context `ctx`.
2237
+
2238
+ Both paths are restricted to environments flagged `allowLocalExecution: true`. A production environment left at the default `false` is never resolvable for a test run.
2239
+
2240
+ ```typescript
2241
+ export const pushWithDb = fixture('push-db', {
2242
+ event: push({ branches: ['main'] }),
2243
+ secrets: { db: 'test-database' }, // 'test-database' must be allowLocalExecution: true
2244
+ });
2245
+ ```
2246
+
2247
+ ```typescript
2248
+ step('migrate', async (ctx) => {
2249
+ const dbUrl = await ctx.secrets.get('KICI_DATABASE_URL');
2250
+ await ctx.$`npx prisma migrate deploy`;
2251
+ });
2252
+ ```
2253
+
2254
+ ### Precedence: CLI values win
2255
+
2256
+ When a key exists in both sources, the **CLI-uploaded local value wins** over the orchestrator test-environment value. This makes a local override a per-run knob: set `--env KICI_DATABASE_URL=...` (or put it in `.kici/.secrets`) to shadow the test environment's value for just that run, without changing anything on the orchestrator.
2257
+
2258
+ ### Fail-closed on non-test environments
2259
+
2260
+ Test-run secret resolution is fail-closed:
2261
+
2262
+ - If a fixture maps a context to an environment that does not exist, the run is **rejected**.
2263
+ - If a fixture maps a context to an environment whose `allowLocalExecution` is `false`, the run is **rejected**.
2264
+ - The `allowLocalExecution` gate applies to **all** remote test runs: a run whose matched workflow targets an environment with the flag off is rejected, so a test run can never resolve production secrets.
2265
+
2266
+ ### The `allowLocalExecution` environment flag
2267
+
2268
+ Each environment carries an `allowLocalExecution` flag (default `false`) that controls test-run access to that environment and to its secrets. Production environments should leave it at `false`; create a dedicated test environment with `allowLocalExecution: true` that binds only test-only secret scopes for the contexts you want test runs to use.
2269
+
2270
+ The flag is set by the orchestrator operator, either via the CLI:
2271
+
2272
+ ```bash
2273
+ kici-admin environment set-policy --env test-database --allow-local-execution true
2274
+ ```
2275
+
2276
+ or via the dashboard's "Test runs" toggle on the environment detail page. `kici secrets list` only surfaces contexts whose owning environment has `allowLocalExecution: true`, so production environments are never advertised as test-accessible.
2277
+
2278
+ ### Local execution as an alternative
2279
+
2280
+ `kici run local` resolves the same local secret files entirely on your machine and honors the fixture `secrets: { ... }` mapping to pick which local context backs each name (see [`kici run local`](https://docs.kici.dev/user/cli-reference/#kici-run-local)). Because the values never leave your machine, it's a good fit when you want to exercise secret-dependent steps without involving the orchestrator at all.
2281
+
2282
+ ### Discovering available contexts
2283
+
2284
+ ```bash
2285
+ # List test-accessible secret contexts and their key names (not values)
2286
+ kici secrets list
2287
+ ```
2288
+
2289
+ ## Detecting test mode in workflows
2290
+
2291
+ Use `ctx.isTestRun` to conditionally skip destructive operations:
2292
+
2293
+ ```typescript
2294
+ step('deploy', async (ctx) => {
2295
+ if (ctx.isTestRun) {
2296
+ ctx.log.info('Skipping deployment in test mode');
2297
+ return;
2298
+ }
2299
+ await ctx.$`kubectl apply -f k8s/`;
2300
+ });
2301
+ ```
2302
+
2303
+ ## Run history
2304
+
2305
+ ### Viewing history
2306
+
2307
+ ```bash
2308
+ # Show recent test runs (from local history)
2309
+ kici run remote --history
2310
+ ```
2311
+
2312
+ ### Run details
2313
+
2314
+ ```bash
2315
+ # Show run summary (reads the Platform, falls back to local history)
2316
+ kici runs show <run-id>
2317
+
2318
+ # Show full logs
2319
+ kici runs logs <run-id>
2320
+
2321
+ # Show logs for a specific job
2322
+ kici runs logs <run-id> --job build
2323
+
2324
+ # Machine-readable output
2325
+ kici runs show <run-id> --json
2326
+ ```
2327
+
2328
+ ## Scaffolding with kici init
2329
+
2330
+ Running `kici init` in a new project scaffolds a sample test fixture alongside the workflow templates:
2331
+
2332
+ ```
2333
+ .kici/
2334
+ workflows/
2335
+ hello-world.ts # Sample workflow
2336
+ pr-checks.ts # Sample PR workflow
2337
+ tests/
2338
+ push-test.ts # Sample push fixture
2339
+ package.json
2340
+ tsconfig.json
2341
+ .kiciignore # Default exclusion patterns
2342
+ ```
2343
+
2344
+ The generated fixture uses the detected default branch:
2345
+
2346
+ ```typescript
2347
+ // .kici/tests/push-test.ts
2348
+ import { fixture, push } from '@kici-dev/sdk';
2349
+
2350
+ export const pushMain = fixture('push-main', {
2351
+ event: push({ branches: ['main'] }),
2352
+ });
2353
+ ```
2354
+
2355
+ ## See also
2356
+
2357
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/) -- complete command reference for all `kici` commands
2358
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) -- trigger functions, step context, and workflow API
2359
+ - [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/) -- example workflows to test against
2360
+
2361
+ ---
2362
+
2363
+ ## Workflow patterns
2364
+
2365
+ Source: https://docs.kici.dev/user/workflow-patterns/
2366
+
2367
+ Practical patterns for building real-world KiCI workflows in TypeScript. The patterns are organised across five pages -- start with [Basic CI](https://docs.kici.dev/user/patterns/basic/) if you're new, or jump to [Integrations](https://docs.kici.dev/user/patterns/integrations/) if you're wiring up a non-GitHub forge or a generic webhook.
2368
+
2369
+ | Page | Covers |
2370
+ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
2371
+ | [Basic CI](https://docs.kici.dev/user/patterns/basic/) | Single-job CI, PR-only / push-only filters, multiple triggers on one workflow, manual-only workflows. |
2372
+ | [Conditionals & matrix](https://docs.kici.dev/user/patterns/conditionals-matrix/) | Conditional execution with rules, matrix builds (static + dynamic), and dynamic job generation. |
2373
+ | [Integrations](https://docs.kici.dev/user/patterns/integrations/) | Workflow chaining, generic webhooks, Stripe handlers, self-hosted git forges (Forgejo / Gitea / Gogs), plain GitHub repo webhooks (no GitHub App). |
2374
+ | [Scheduling & events](https://docs.kici.dev/user/patterns/scheduling-and-events/) | Nightly cron, workflow-complete-triggered deploys, custom event chaining. |
2375
+ | [Pattern reference](https://docs.kici.dev/user/patterns/reference/) | Step context, the examples repository, and GitHub check run output -- cross-cutting reference shared by every pattern above. |
2376
+
2377
+ ## See also
2378
+
2379
+ - [Event system](https://docs.kici.dev/user/events/) -- event model concepts, registration model, circuit breaker
2380
+ - [SDK reference](https://docs.kici.dev/user/sdk-reference/) -- complete API reference for all functions used in these patterns
2381
+ - [CLI reference](https://docs.kici.dev/user/cli-reference/) -- how to compile and test these workflows locally
2382
+ - [Getting started](https://docs.kici.dev/user/getting-started/) -- installation and first workflow setup
2383
+ - [Job execution lifecycle](https://docs.kici.dev/architecture/execution/job-execution/) -- how agents execute the jobs defined in these patterns
2384
+ - [GitHub checks architecture](https://docs.kici.dev/architecture/webhooks/github-checks/) -- deep dive into the check run system
2385
+
2386
+ ---