@kici-dev/compiler 0.1.3 → 0.1.5

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.
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import "./commands/index.js";
22
22
  import { Argument, Command, Option } from "commander";
23
23
  import pc from "picocolors";
24
24
  //#region src/cli.ts
25
- const version = "0.1.3";
25
+ const version = "0.1.5";
26
26
  const program = new Command();
27
27
  program.name("kici").description("KiCI workflow compiler").version(version);
28
28
  program.hook("preAction", () => {
@@ -50,7 +50,7 @@ program.command("fixture").addArgument(fixtureEventArg).description("Generate fi
50
50
  await fixtureCommand(event, options);
51
51
  });
52
52
  const runCommand = program.command("run").description("Execute workflows locally or remotely");
53
- runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").action(async (event, options) => {
53
+ runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open, schedule) — optional with --pick").description("Execute workflows locally without orchestrator infrastructure").option("-p, --pick", "Interactively pick a workflow and trigger to simulate", false).option("--workflow <name>", "Run only the specified workflow").option("--job <name>", "Run only the specified job (and its dependencies)").option("--branch <name>", "Override detected git branch").option("--sha <hash>", "Override detected git SHA").option("--payload <path>", "Path to explicit event payload JSON file").option("--concurrency <n>", "Max parallel jobs (default: CPU cores)", parseInt).option("--keep-going", "Continue after job failure", false).option("--container", "Use Podman container isolation", false).option("--env <KEY=VALUE>", "Environment variable override (repeatable)", (val, prev) => [...prev, val], []).option("--quiet", "Suppress streaming output", false).option("--json", "Output structured JSON result", false).option("--junit <path>", "Output JUnit XML result").option("--files <path>", "Override changed file paths (repeatable, default: git diff)", (val, prev) => [...prev, val], []).option("--debug", "Verbose internals", false).option("--kici-dir <path>", "Path to .kici directory", ".kici").option("--in-place", "Run against the real working directory instead of an isolated tmp checkout", false).option("--keep", "Always retain the isolated tmp checkout (default: keep only on failure)", false).action(async (event, options) => {
54
54
  if (options.pick && options.workflow) {
55
55
  console.error("Error: --pick is mutually exclusive with --workflow.");
56
56
  process.exit(2);
@@ -76,7 +76,9 @@ runCommand.command("local").argument("[event]", "Event type (e.g., push, pr:open
76
76
  junit: options.junit,
77
77
  files: options.files,
78
78
  debug: options.debug,
79
- kiciDir: options.kiciDir
79
+ kiciDir: options.kiciDir,
80
+ inPlace: options.inPlace,
81
+ keep: options.keep
80
82
  });
81
83
  process.exit(success ? 0 : 1);
82
84
  });
@@ -48,16 +48,15 @@ async function oauthLogin(options) {
48
48
  const platformUrl = options.platformEndpoint || process.env.KICI_PLATFORM_URL || "";
49
49
  const issuer = process.env.KICI_OIDC_ISSUER || "";
50
50
  const clientId = process.env.KICI_OIDC_CLIENT_ID || "";
51
- const projectId = process.env.KICI_OIDC_PROJECT_ID || "";
52
51
  const missing = [];
53
52
  if (!platformUrl) missing.push("KICI_PLATFORM_URL");
54
53
  if (!issuer) missing.push("KICI_OIDC_ISSUER");
55
54
  if (!clientId) missing.push("KICI_OIDC_CLIENT_ID");
56
55
  if (missing.length > 0) {
57
56
  console.error(pc.red(`Error: missing required env var(s) for OAuth login: ${missing.join(", ")}`));
58
- console.error(pc.gray(" Set them to your Platform and Zitadel endpoints, for example:"));
57
+ console.error(pc.gray(" Set them to your Platform and IdP endpoints, for example:"));
59
58
  console.error(pc.gray(" export KICI_PLATFORM_URL=https://your-platform.example.com"));
60
- console.error(pc.gray(" export KICI_OIDC_ISSUER=https://your-zitadel.example.com"));
59
+ console.error(pc.gray(" export KICI_OIDC_ISSUER=https://your-idp.example.com"));
61
60
  console.error(pc.gray(" export KICI_OIDC_CLIENT_ID=<cli-client-id>"));
62
61
  return false;
63
62
  }
@@ -66,12 +65,11 @@ async function oauthLogin(options) {
66
65
  const useDeviceFlow = options.device || !browserCmdSet && isHeadless();
67
66
  const flowName = useDeviceFlow ? "device authorization" : "PKCE (browser)";
68
67
  console.log(pc.gray(` Using ${flowName} flow`));
69
- console.log(pc.cyan(`\n Step 2/4: Authenticating with ZITADEL (${flowName})...`));
68
+ console.log(pc.cyan(`\n Step 2/4: Authenticating with IdP (${flowName})...`));
70
69
  let accessToken;
71
70
  const oauthOpts = {
72
71
  issuer,
73
- clientId,
74
- projectId: projectId || void 0
72
+ clientId
75
73
  };
76
74
  if (useDeviceFlow) accessToken = await deviceFlow(oauthOpts);
77
75
  else accessToken = await pkceFlow(oauthOpts);
@@ -15,7 +15,7 @@ import { formatDuration, logger, toErrorMessage } from "@kici-dev/shared";
15
15
  * Looks up local history first, then fetches from the orchestrator for
16
16
  * up-to-date status and logs.
17
17
  */
18
- const CLI_VERSION = "0.1.3";
18
+ const CLI_VERSION = "0.1.5";
19
19
  /**
20
20
  * Show status and details of a test run.
21
21
  *
@@ -14,7 +14,7 @@ import { pathToFileURL } from "node:url";
14
14
  * in its sandbox process). No Rolldown bundling: host-repo imports and
15
15
  * transitive deps with dynamic import() resolve via Node's normal ESM loader
16
16
  * against the workspace's node_modules — the same module graph any other
17
- * `npx tsx` invocation would see. This keeps ops-style workflows (which
17
+ * `pnpm exec tsx` invocation would see. This keeps ops-style workflows (which
18
18
  * import repo-local modules like scripts/lib/*) working the same as minimal
19
19
  * workflows that only touch @kici-dev/sdk.
20
20
  */
@@ -449,6 +449,41 @@ The lock file approach means the orchestrator stays git-agnostic -- it only need
449
449
 
450
450
  ---
451
451
 
452
+ ## 5-minute quickstart
453
+
454
+ Source: https://kici.dev/docs/user/quickstart/
455
+
456
+ KiCI offers two equally-supported quickstart paths. Pick the one that fits your machine — both end with the same working pipeline (orchestrator + agent + your first workflow run visible in the dashboard).
457
+
458
+ ## Option A — Docker / Podman (recommended)
459
+
460
+ Two containers brought up with `docker compose up -d` (orchestrator + PostgreSQL), plus one short-lived agent container spawned per job by the container scaler. Minimal host setup, perfect for a laptop, home server, or a tiny VM. No need to install PostgreSQL or any other system service.
461
+
462
+ [Start with the Docker / Podman quickstart →](./quickstart/compose.md)
463
+
464
+ ## Option B — Bare-metal install
465
+
466
+ Native systemd services managed by `kici-admin orchestrator install` / `kici-admin agent install` — the orchestrator and agents run as native processes. The backing PostgreSQL runs as a single container by default (one `docker compose up -d`), or you can install it natively if you'd rather not run a container runtime at all. Best for a long-lived Linux host.
467
+
468
+ [Start with the bare-metal quickstart →](./quickstart/bare-metal.md)
469
+
470
+ ## Which should I pick?
471
+
472
+ | | Docker / Podman | Bare metal |
473
+ | ----------------- | ---------------------------------------- | ------------------------------------------------------------------------------------- |
474
+ | Host requirements | `docker` or `podman` with compose v2.20+ | systemd, Node.js 24+, PostgreSQL 18 (container — needs `docker`/`podman` — or native) |
475
+ | Time to first run | ~5 minutes | ~10 minutes |
476
+ | Upgrades | `docker compose pull` + restart | `kici-admin orchestrator restart` after `npm install -g kici-admin@latest` |
477
+ | Best for | Quick evaluation, ephemeral hosts | Long-lived production hosts |
478
+
479
+ If you're not sure, pick Docker / Podman.
480
+
481
+ ## Looking for the laptop-only path?
482
+
483
+ Both quickstarts deploy a real orchestrator + agent. If you only want to write a workflow and dry-run it on your laptop with no infrastructure, [Getting started](./getting-started.md) covers `kici test` and `kici run local` instead.
484
+
485
+ ---
486
+
452
487
  # Workflow patterns
453
488
 
454
489
  ## Basic workflow patterns
@@ -1390,7 +1425,7 @@ const build = step('build', {
1390
1425
 
1391
1426
  ## Examples repository
1392
1427
 
1393
- For more runnable examples, see the [examples/](https://github.com/kici-dev/kici/tree/master/examples) directory in the KiCI repository.
1428
+ For more runnable examples, see the [examples/](https://github.com/kici-dev/kici-public/tree/main/examples) directory in the KiCI repository.
1394
1429
 
1395
1430
  ## GitHub check run output
1396
1431
 
@@ -3452,7 +3487,7 @@ function genericWebhook(config: GenericWebhookConfigInput): GenericWebhookTrigge
3452
3487
 
3453
3488
  ```typescript
3454
3489
  interface GenericWebhookConfigInput {
3455
- source: string; // Required: webhook source identifier
3490
+ source: string; // Required: must match `--name` from `kici-admin source add generic`
3456
3491
  events?: string[]; // Filter by event types
3457
3492
  match?: Record<string, unknown>; // JSONPath payload matching
3458
3493
  not?: Record<string, unknown>; // Negative JSONPath filter
@@ -3873,7 +3908,7 @@ The KiCI CLI supports three authentication methods: browser-based OAuth (default
3873
3908
 
3874
3909
  The default `kici login` flow:
3875
3910
 
3876
- 1. Opens your default browser to the KiCI identity provider (ZITADEL)
3911
+ 1. Opens your default browser to the KiCI identity provider
3877
3912
  2. You authenticate in the browser
3878
3913
  3. The CLI receives a token via localhost callback
3879
3914
  4. A personal access token (PAT) is created and stored locally
@@ -4012,20 +4047,20 @@ The Platform exposes a versioned REST API under `/api/v1/*`. The same endpoints
4012
4047
 
4013
4048
  Every request to `/api/v1/*` carries an `Authorization: Bearer <token>` header. The Platform routes on the prefix:
4014
4049
 
4015
- | Prefix | Token type | Created via | Scope |
4016
- | ----------- | -------------------------------- | --------------------------------------- | ---------------- |
4017
- | `kici_pat_` | Personal access token | `kici login` or dashboard | User (cross-org) |
4018
- | `kici_sk_` | User API key | Dashboard → Settings → API keys | Org |
4019
- | `kici_sa_` | Service account key | Dashboard → Settings → Service accounts | Org |
4020
- | (other) | ZITADEL JWT or opaque OIDC token | OIDC login (browser SPA) | User (cross-org) |
4050
+ | Prefix | Token type | Created via | Scope |
4051
+ | ----------- | ----------------------------- | --------------------------------------- | ---------------- |
4052
+ | `kici_pat_` | Personal access token | `kici login` or dashboard | User (cross-org) |
4053
+ | `kici_sk_` | User API key | Dashboard → Settings → API keys | Org |
4054
+ | `kici_sa_` | Service account key | Dashboard → Settings → Service accounts | Org |
4055
+ | (other) | OIDC JWT or opaque OIDC token | OIDC login (browser SPA) | User (cross-org) |
4021
4056
 
4022
- JWT and opaque OIDC tokens are validated against your ZITADEL issuer (JWKS for JWTs, `/oidc/v1/userinfo` for opaque ones). All `kici_*` tokens are validated by SHA-256 hash lookup against the Platform DB. See [RBAC: authentication methods](../architecture/security/rbac.md#authentication-methods) for the full model.
4057
+ JWT and opaque OIDC tokens are validated against the configured OIDC issuer (JWKS for JWTs, the issuer's UserInfo endpoint for opaque ones). All `kici_*` tokens are validated by SHA-256 hash lookup against the Platform DB. See [RBAC: authentication methods](../architecture/security/rbac.md#authentication-methods) for the full model.
4023
4058
 
4024
4059
  > **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.
4025
4060
 
4026
4061
  ### Permissions
4027
4062
 
4028
- 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 13 resources and 5 levels are documented in [RBAC](../architecture/security/rbac.md#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
4063
+ Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The 14 resources and 5 levels are documented in [RBAC](../architecture/security/rbac.md#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
4029
4064
 
4030
4065
  ### Configurable surfaces
4031
4066
 
@@ -4232,6 +4267,8 @@ kici run local [event] [options]
4232
4267
  | `--junit <path>` | none | Output JUnit XML result to file |
4233
4268
  | `--debug` | `false` | Verbose internals |
4234
4269
  | `--kici-dir <path>` | `.kici` | Path to .kici directory |
4270
+ | `--in-place` | `false` | Run against the real working directory instead of an isolated tmp checkout (see "Execution isolation" below) |
4271
+ | `--keep` | `false` | Always retain the isolated tmp checkout (default: keep only on failure) |
4235
4272
 
4236
4273
  **Interactive workflow selection (`--pick` / `-p`):**
4237
4274
 
@@ -4265,6 +4302,26 @@ Coordination is local only — running the same workflow on two different machin
4265
4302
 
4266
4303
  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.
4267
4304
 
4305
+ **Execution isolation:**
4306
+
4307
+ 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.
4308
+
4309
+ 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`.
4310
+
4311
+ The path is logged at run start (for example, `running in /tmp/kici-run-ab12cd`) so you can inspect it.
4312
+
4313
+ Cleanup policy:
4314
+
4315
+ - On a fully successful run, the isolated checkout is removed.
4316
+ - On failure, it is retained and its path is logged so you can inspect the failed state.
4317
+ - `--keep` always retains it, even on success.
4318
+
4319
+ Set the `KICI_RUN_DIR` environment variable to place the isolated checkout under a base directory other than the system temp directory.
4320
+
4321
+ 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.
4322
+
4323
+ 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.
4324
+
4268
4325
  **Examples:**
4269
4326
 
4270
4327
  ```bash
@@ -4453,7 +4510,7 @@ kici test push --files src/index.ts --files README.md
4453
4510
 
4454
4511
  Authenticate with KiCI via browser-based OAuth (default) or API key (`--token`).
4455
4512
 
4456
- By default, `kici login` opens your browser for ZITADEL 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.
4513
+ 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.
4457
4514
 
4458
4515
  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`).
4459
4516
 
@@ -5318,7 +5375,7 @@ The lock file (`kici.lock.json`) is a JSON file with the following top-level fie
5318
5375
 
5319
5376
  | Field | Description |
5320
5377
  | --------------- | ------------------------------------------------------------------------------------------------------------------- |
5321
- | `schemaVersion` | Lock file schema version (currently 11). Incremented on breaking format changes. |
5378
+ | `schemaVersion` | Lock file schema version (currently 12). Incremented on breaking format changes. |
5322
5379
  | `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
5323
5380
  | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
5324
5381
  | `lockfileHash` | SHA-256 of `.kici/package-lock.json`, used as the dependency cache key. Omitted when no package lock file exists. |
@@ -6068,6 +6125,7 @@ The left sidebar provides persistent navigation across all org-scoped pages:
6068
6125
  - **Secrets** -- secret scope management with environment bindings
6069
6126
  - **Approval queue** -- held runs pending approval (shows a badge with pending count)
6070
6127
  - **Activity** -- federated forensic log merging upstream tenant-plane mutations and orchestrator reads (`access_log`) into one chronological stream
6128
+ - **DLQ** -- dead-letter queue of internal events whose dispatch retries were exhausted (shows a badge with the current depth)
6071
6129
  - **Settings** -- organization settings with tabbed sub-pages
6072
6130
 
6073
6131
  The sidebar footer shows the WebSocket connection indicator, your user profile, UTC/local time toggle, theme toggle, and a collapse button.
@@ -6445,7 +6503,7 @@ Each member's linked provider accounts (e.g. GitHub) are also visible here.
6445
6503
 
6446
6504
  <!-- help:settings-roles#settings -->
6447
6505
 
6448
- Roles define granular permissions across 13 resource categories (runs, secrets, members, etc.) with 5 access levels: `none`, `read`, `read_payload`, `write`, `admin`.
6506
+ Roles define granular permissions across 14 resource categories (runs, secrets, members, etc.) with 5 access levels: `none`, `read`, `read_payload`, `write`, `admin`.
6449
6507
 
6450
6508
  Create custom roles to restrict what team members can do, or use the built-in **Owner** role for full access.
6451
6509
 
@@ -6521,6 +6579,16 @@ When you hit the cap, new relayed webhooks are rejected with `429 Plan limit rea
6521
6579
 
6522
6580
  <!-- /help:settings-billing-relayed-webhooks -->
6523
6581
 
6582
+ <!-- help:settings-billing-currency#settings -->
6583
+
6584
+ Switch the prices shown on the tier cards between US dollars and euros. The choice you pick here is also the currency Stripe charges in when you click "Upgrade".
6585
+
6586
+ The default is detected from your browser language. EU, EFTA, and UK locales default to euros; everywhere else defaults to dollars.
6587
+
6588
+ Your choice persists in a 90-day cookie (`kici_pricing_currency`), so it survives across reloads and applies on every billing page.
6589
+
6590
+ <!-- /help:settings-billing-currency -->
6591
+
6524
6592
  <!-- help:billing-payment-failure#settings -->
6525
6593
 
6526
6594
  This banner appears when your organization's latest payment to Stripe has failed. Your subscription remains active during the retry period, but you should update your payment method promptly to avoid service interruption.
@@ -6549,6 +6617,23 @@ Click a row's run target to jump straight to the run detail page.
6549
6617
 
6550
6618
  <!-- /help:activity-filters -->
6551
6619
 
6620
+ ## DLQ
6621
+
6622
+ The DLQ (dead-letter queue) page lists internal events whose dispatch attempts were exhausted (or that hit a non-retryable error). Each row shows when the event landed in the DLQ, the event name, the attempt count, the failure reason, and the last error message.
6623
+
6624
+ <!-- help:dlq#dlq -->
6625
+
6626
+ The DLQ holds events your org emitted that could not be dispatched within the retry budget. The sidebar badge shows the current depth so you can spot a building backlog without opening the page.
6627
+
6628
+ Per-row actions (visible when you have `event_dlq:write`):
6629
+
6630
+ - **Retry:** clears the DLQ flag and re-publishes the event. A healthy orchestrator picks it up immediately.
6631
+ - **Discard:** permanently deletes the row. Use when the payload is corrupt or the routing target no longer exists.
6632
+
6633
+ Members with only `event_dlq:read` see the list but cannot retry or discard. Org owners have both actions by default.
6634
+
6635
+ <!-- /help:dlq -->
6636
+
6552
6637
  <!-- help:settings-ci-trust#settings -->
6553
6638
 
6554
6639
  CI trust policy controls how your organization handles PR-triggered runs from different contributor types. Configure the default trust level for unknown contributors and set per-member overrides to control who can run workflows with full secrets access.
@@ -6643,6 +6728,22 @@ For each endpoint you can:
6643
6728
 
6644
6729
  <!-- /help:settings-webhooks -->
6645
6730
 
6731
+ <!-- help:settings-security-dashboard-policy#settings -->
6732
+
6733
+ Read-only view of the orchestrator's dashboard-write policy.
6734
+
6735
+ Each row toggles one mutating dashboard action — setting a secret, approving a held run, retrying a dead-lettered webhook, and so on. The orchestrator operator decides which actions stay on the dashboard and which become **CLI-only**. The dashboard cannot change the policy itself — that's the point: disabled actions stay out of the SaaS Platform's trust path.
6736
+
6737
+ Manage the policy with:
6738
+
6739
+ - **Show the full policy:** `kici-admin org-settings dashboard-writes show`
6740
+ - **Disable an operation:** `kici-admin org-settings dashboard-writes set --op <name>=false`
6741
+ - **Reset to permissive defaults:** `kici-admin org-settings dashboard-writes reset`
6742
+
6743
+ The summary strip at the top shows total / enabled / disabled counts plus whether your orchestrator is currently connected. A disconnected orchestrator means the page falls back to the cached policy from the most recent connection.
6744
+
6745
+ <!-- /help:settings-security-dashboard-policy -->
6746
+
6646
6747
  <!-- help:settings-webhooks-delivery-log#settings -->
6647
6748
 
6648
6749
  The delivery log shows recent webhook deliveries for an endpoint, including the HTTP status code, number of retry attempts, and the event payload.
@@ -6698,6 +6799,7 @@ The settings page (`/orgs/:customerId/settings`) uses a tabbed layout:
6698
6799
  9. **Global workflows** -- org-level security knobs for cross-repo workflows (visible with `org_settings:read` permission)
6699
6800
  10. **Webhooks** -- outbound webhook endpoint management with delivery logs and test ping
6700
6801
  11. **Event log** -- inbound webhook delivery log (visible with `event_log:read` permission)
6802
+ 12. **Security** -- read-only view of the orchestrator's dashboard-write policy matrix (visible with `org_settings:read` permission)
6701
6803
 
6702
6804
  Audit-log-style entries are no longer a settings tab; they live on the dedicated **Activity** page accessible from the sidebar.
6703
6805
 
@@ -6793,6 +6895,28 @@ Organizations are the top-level container for your CI/CD resources. Each org has
6793
6895
 
6794
6896
  <!-- /help:orgs-list -->
6795
6897
 
6898
+ <!-- help:orchestrators-list#orchestrators -->
6899
+
6900
+ The Orchestrators page lists every orchestrator currently connected to this org, keyed by **cluster name**. Each row shows:
6901
+
6902
+ - **Cluster** — the human-friendly cluster name set on the orch via `kici-admin cluster-name set <name>`, or an auto-generated `cluster-<6hex>` if no operator has renamed it.
6903
+ - **Role** — `coordinator` (talks to Platform directly) or `worker` (relays through a coordinator).
6904
+ - **Version**, **mode**, **routing keys**, and **last heartbeat**.
6905
+
6906
+ Click a cluster to drill into its per-orch surfaces (security policy, environments, secrets, DLQ, registrations, global workflows). Different clusters in the same org can have different settings — this page is the entry point that lets you pick which cluster you're configuring.
6907
+
6908
+ <!-- /help:orchestrators-list -->
6909
+
6910
+ <!-- help:orchestrators-scope#orchestrators -->
6911
+
6912
+ Every panel inside this view scopes to the named cluster. Settings shown here come from that orchestrator's own database — a sibling orchestrator in the same org may have a different security policy, different environments, and different secrets.
6913
+
6914
+ When the cluster shows **disconnected**, the orch is offline and its current state can't be queried. Most child pages will return 404 in that state; return to the orchestrator list to find a connected cluster.
6915
+
6916
+ To rename a cluster, run `kici-admin cluster-name set <new>` on the orchestrator host and restart the orch service so the new name reaches Platform on the next `source.register`.
6917
+
6918
+ <!-- /help:orchestrators-scope -->
6919
+
6796
6920
  ## Workflows
6797
6921
 
6798
6922
  The workflows page (`/orgs/:customerId/workflows`) shows permanently registered workflows listening for events. It displays a filterable table with columns for workflow name, repository, trigger types, last triggered time, next fire time (for scheduled workflows), source repos, and actions.
@@ -6841,6 +6965,22 @@ The secrets page (`/orgs/:customerId/secrets`) provides a scope-centric view of
6841
6965
 
6842
6966
  Permission-gated: `secrets:read` to view scopes, `secrets:write` to add or delete secrets, `environments:write` to modify environment bindings.
6843
6967
 
6968
+ ### Where secrets live
6969
+
6970
+ Secret values are stored in the orchestrator's secret store and authorized through the orchestrator's RBAC. The dashboard surfaces secret **names** and scope membership for every secret regardless of where the value was entered.
6971
+
6972
+ Whether secret **values** can be set from the dashboard depends on the orchestrator's [dashboard-write policy](/operator/security/dashboard-write-policy):
6973
+
6974
+ - **Permissive (default):** the "Add secret" and "Edit value" controls accept plaintext directly in the dashboard. This is how a typical SaaS CI tool works and is the right default for small teams.
6975
+ - **`secrets.set` disabled by policy:** the controls render with a lock icon, grayed out. Hovering shows the exact `kici-admin secret set` invocation needed; a copy button puts it on the clipboard. The control is inert — the dashboard issues no mutating request. Use the CLI to enter values; the dashboard refreshes within ~30 seconds and shows the new secret name.
6976
+
6977
+ The policy state is visible at three layers in the UI:
6978
+
6979
+ - A **lock-icon prefix** on every disabled control, with a per-control CLI hint.
6980
+ - A **per-page banner** on any page containing at least one disabled operation, listing every disabled op on that page and its CLI equivalent.
6981
+
6982
+ The Security policy page (Settings → Security → Dashboard policy) renders the full 23-row read-only matrix with the current state and the `kici-admin` command for each row. The policy itself cannot be changed from the dashboard — the orchestrator operator manages it via `kici-admin org-settings dashboard-writes`. See [Dashboard-write policy](/operator/security/dashboard-write-policy) for the operator-side details.
6983
+
6844
6984
  ## Approval queue
6845
6985
 
6846
6986
  The approval queue page (`/orgs/:customerId/approval-queue`) shows held runs that are pending approval. Runs can be held due to environment protection rules (required reviewers, wait timers). The page supports filtering by status (pending, approved, rejected, expired) and provides approve/reject actions for users with `environments:write` permission. Users with `environments:admin` permission can skip wait timers.
@@ -7067,7 +7207,7 @@ kici login --token <<< "$KICI_API_KEY"
7067
7207
  Point the CLI to a self-hosted or testing OIDC provider:
7068
7208
 
7069
7209
  ```bash
7070
- export KICI_OIDC_ISSUER=https://your-zitadel.example.com
7210
+ export KICI_OIDC_ISSUER=https://your-idp.example.com
7071
7211
  export KICI_OIDC_CLIENT_ID=your-client-id
7072
7212
  export KICI_OIDC_PROJECT_ID=your-project-id
7073
7213
  export KICI_PLATFORM_URL=https://your-platform.example.com
@@ -7510,7 +7650,7 @@ genericWebhook({
7510
7650
 
7511
7651
  **Config options:** `source` (required), `events`, `match`, `not`, `auth`, `path`, `description`.
7512
7652
 
7513
- Generic webhook sources must be created by an operator via the admin API before events can be received. See [Operator guide: event routing](../operator/event-routing.md) for setup instructions.
7653
+ The `source` field MUST match the `--name` that an operator passed to `kici-admin source add generic --name <name>` when the source was created — that string is the source's identifier in the orchestrator. Generic webhook sources must be created by an operator before events can be received; see [Operator guide: event routing](../operator/event-routing.md) for setup instructions.
7514
7654
 
7515
7655
  ### Schedule events
7516
7656
 
@@ -7658,7 +7798,7 @@ KiCI's event router delivers every accepted event with **at-least-once** semanti
7658
7798
  is released on failure) and the event is automatically retried.
7659
7799
  - The retry policy is exponential backoff with full jitter: base 5 s, cap 5 min,
7660
7800
  up to 5 attempts before the event lands in the **DLQ** (dead-letter queue).
7661
- Operators triage DLQ entries via `kici-admin event-dlq list / retry / discard`.
7801
+ Operators triage DLQ entries via `kici-admin event-dlq list / count / retry / discard`.
7662
7802
 
7663
7803
  **What this means for workflow authors:**
7664
7804
 
@@ -8151,6 +8291,60 @@ Secrets are managed per-environment in the orchestrator (see [operator docs](/op
8151
8291
 
8152
8292
  This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
8153
8293
 
8294
+ ## Where secret values come from
8295
+
8296
+ Secret values are written either through the dashboard or through `kici-admin` running against the orchestrator. The orchestrator operator decides — per organization — which surface accepts secret writes. From the workflow author's perspective, the resolution path at run time is identical either way; the difference is where you (or your ops team) **enter** the value.
8297
+
8298
+ ### Default — dashboard or CLI
8299
+
8300
+ A fresh orchestrator starts in **permissive** mode: both surfaces are available.
8301
+
8302
+ - **Dashboard:** Settings → Secrets → pick a scope → enter the secret name and value.
8303
+ - **CLI:** `kici-admin secret set --scope <scope> <KEY>` against the orchestrator's HTTP admin API.
8304
+
8305
+ Use whichever fits the workflow — most small teams stay on the dashboard; ops engineers and CI scripts use the CLI.
8306
+
8307
+ ### When the operator has disabled dashboard writes
8308
+
8309
+ The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](/operator/security/dashboard-write-policy). When that flip is on:
8310
+
8311
+ - The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Clicking them shows a tooltip with the exact `kici-admin secret set` invocation needed.
8312
+ - The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
8313
+ - `kici-admin secret set` becomes the single entry point for new and updated secret values.
8314
+
8315
+ This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, environment bindings).
8316
+
8317
+ ### CLI input modes
8318
+
8319
+ `kici-admin secret set` accepts five input modes — pick the one that fits your workflow:
8320
+
8321
+ ```bash
8322
+ # Interactive prompt (default when stdin is a TTY). No echo, no shell history.
8323
+ kici-admin secret set --scope production DB_PASSWORD --prompt
8324
+
8325
+ # Pipe from another tool (default when stdin is not a TTY).
8326
+ pass show prod/db | kici-admin secret set --scope production DB_PASSWORD --from-stdin
8327
+
8328
+ # Read from a file (handy after `sops -d` to a tmpfile).
8329
+ kici-admin secret set --scope production DB_PASSWORD --from-file ./db.pass
8330
+
8331
+ # Read from a named environment variable (CI-friendly).
8332
+ KICI_SECRET_VALUE=$(my-secrets-fetcher prod db) \
8333
+ kici-admin secret set --scope production DB_PASSWORD --from-env KICI_SECRET_VALUE
8334
+
8335
+ # Direct argv — discouraged. Prints a stderr warning ("visible in shell history").
8336
+ kici-admin secret set --scope production DB_PASSWORD --value "<plaintext>"
8337
+ ```
8338
+
8339
+ Two cross-cutting flags help every mode:
8340
+
8341
+ - `--confirm-fingerprint <hex>` — pre-compute SHA-256 of the value and pass it. The CLI rejects the call if the value's fingerprint doesn't match. Catches paste corruption.
8342
+ - `--dry-run` — parse and validate the value, print `[dry-run] would set <key> in scope <scope> sha256=<hex>`, exit without writing.
8343
+
8344
+ `kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
8345
+
8346
+ A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](/operator/security/dashboard-write-policy#cli-input-modes-for-the-plaintext-path).
8347
+
8154
8348
  ## Accessing secrets
8155
8349
 
8156
8350
  Use `ctx.secrets.get(key)` to retrieve a secret value. The method is async to support process-level step isolation in future versions.
@@ -9529,13 +9723,13 @@ GitHub Platform Orchestrator Agent
9529
9723
 
9530
9724
  5. **Agent execution:** The agent wraps each `onJobDispatch` callback in `requestContext.run()` with `requestId`, `runId`, and `jobId`. A trace header is printed once at job start. All subsequent log lines carry all trace fields automatically.
9531
9725
 
9532
- 6. **Check run summaries:** GitHub Check Run updates include `Trace: <requestId> | Run: <runId>` in the summary text, giving operators a direct link from GitHub UI to ELK queries.
9726
+ 6. **Check run summaries:** GitHub Check Run updates include `Trace: <requestId> | Run: <runId>` in the summary text, giving operators a direct link from GitHub UI to Loki queries.
9533
9727
 
9534
9728
  ### Implementation
9535
9729
 
9536
- Trace propagation uses Node.js `AsyncLocalStorage` from `@kici-dev/shared`. A logger format (`traceContextFormat`) reads the current context and injects fields into every JSON log line -- no changes needed at individual call sites.
9730
+ Trace propagation uses Node.js `AsyncLocalStorage` from `@kici-dev/shared`. A logger format reads the current context and injects fields into every JSON log line -- no changes needed at individual call sites.
9537
9731
 
9538
- Tier identification is handled at the infrastructure level: `kici.service` (populated by Filebeat from the container name) identifies which service produced the log. For agent logs forwarded through the orchestrator's stdout, the `app.service` field (set to `'agent'` by the log forwarder) identifies the originating tier.
9732
+ Tier identification is handled at the infrastructure level: the `service` Loki label (set by Grafana Alloy from the systemd unit / log source) identifies which service produced the log (`platform`, `orchestrator`, `agent`, etc.). For agent logs forwarded through the orchestrator's stdout, the parsed JSON also carries an inner `service: 'agent'` field — query both with `{service="orchestrator"} | json | service="agent"` to disambiguate.
9539
9733
 
9540
9734
  ## Output chaining data flow
9541
9735
 
@@ -9633,7 +9827,7 @@ The Platform tier exposes a `/ws/browser` WebSocket endpoint for dashboard clien
9633
9827
  - [State machine](./execution/state-machine.md) -- job execution state transitions
9634
9828
  - [Webhook delivery](./webhooks/webhook-delivery.md) -- detailed webhook processing pipeline
9635
9829
  - [Operator: dependency caching](../operator/dependency-caching.md) -- configuration guide
9636
- - [Operator: monitoring & tracing](../operator/observability/monitoring.md) -- trace fields and ELK queries
9830
+ - [Operator: monitoring & tracing](../operator/observability/monitoring.md) -- trace fields and Loki queries
9637
9831
  - [Operator: event routing & generic webhooks](../operator/event-routing.md) -- generic source setup and trust management
9638
9832
  - [SDK reference: output chaining](../user/sdk/core.md#output-chaining) -- user-facing output chaining API
9639
9833
 
@@ -9885,7 +10079,7 @@ Shared utilities used across packages. Provides `initZx()` for zx initialization
9885
10079
 
9886
10080
  ### Dashboard
9887
10081
 
9888
- Web UI for KiCI. A browser single-page application that provides the operator dashboard with execution run listing, run detail views, real-time log streaming, settings management, and keyboard shortcut support. Authenticates via OIDC against ZITADEL and communicates with the Platform REST-over-WebSocket API.
10082
+ Web UI for KiCI. A browser single-page application that provides the operator dashboard with execution run listing, run detail views, real-time log streaming, settings management, and keyboard shortcut support. Authenticates via OIDC against the identity provider and communicates with the Platform REST-over-WebSocket API.
9889
10083
 
9890
10084
  ### `kici` (wrapper)
9891
10085
 
@@ -9961,7 +10155,7 @@ The agent connects outbound to the orchestrator WebSocket endpoint. After regist
9961
10155
 
9962
10156
  ## Authentication and multi-tenancy
9963
10157
 
9964
- KiCI uses application-level tenant isolation. The Platform dashboard API accepts three authentication methods (PATs, API keys, JWTs) and enforces org membership on every `/api/v1/orgs/:customerId/*` request through `orgContextMiddleware`. Triple-auth wiring, the ZITADEL identity provider integration, and the API key permission matrix are SaaS-Platform specifics covered in the internal docs.
10158
+ KiCI uses application-level tenant isolation. The Platform dashboard API accepts three authentication methods (PATs, API keys, JWTs) and enforces org membership on every `/api/v1/orgs/:customerId/*` request.
9965
10159
 
9966
10160
  ## See also
9967
10161
 
@@ -8,6 +8,7 @@ The full markdown bundle of every page indexed here is available at https://kici
8
8
 
9
9
  - [User guide](https://kici.dev/docs/user/): Writing and testing CI/CD workflows in TypeScript
10
10
  - [Getting started with KiCI](https://kici.dev/docs/user/getting-started/): Install the SDK, write your first workflow, compile and test locally
11
+ - [5-minute quickstart](https://kici.dev/docs/user/quickstart/): Stand up a KiCI orchestrator + agent and run your first workflow
11
12
 
12
13
  ## Workflow patterns
13
14
 
@@ -9,9 +9,11 @@ import { generateEventPayload } from "./payload-generator.js";
9
9
  import { executeResolvedJob, resolveJobs } from "./job-runner.js";
10
10
  import { displayLocalSummary, formatLocalJsonResult, formatLocalJunitResult } from "./output-streamer.js";
11
11
  import { PickerCancelledError, runPicker } from "./picker.js";
12
+ import { materializeCheckout } from "./materializer.js";
12
13
  import { ConcurrencyKeyEvaluationError, acquireWorkflowLock } from "./workflow-lock.js";
13
14
  import pc from "picocolors";
14
15
  import { writeFile } from "node:fs/promises";
16
+ import path from "node:path";
15
17
  import { logger } from "@kici-dev/shared";
16
18
  import { matchAllWorkflows } from "@kici-dev/engine";
17
19
  import os from "node:os";
@@ -165,6 +167,7 @@ async function runWorkflowBody(workflow, ctx, options, secrets, kiciDir, concurr
165
167
  event,
166
168
  secrets,
167
169
  kiciDir,
170
+ execDir: ctx.execDir,
168
171
  jobOutputsMap,
169
172
  signal
170
173
  });
@@ -265,6 +268,14 @@ async function executeLocal(options) {
265
268
  }
266
269
  }
267
270
  const isQuiet = Boolean(options.quiet || options.json);
271
+ const repoRoot = path.dirname(kiciDir);
272
+ let materialized = null;
273
+ let execDir = repoRoot;
274
+ if (!options.inPlace) {
275
+ materialized = await materializeCheckout(repoRoot, { runDir: process.env.KICI_RUN_DIR });
276
+ execDir = materialized.path;
277
+ logger.info(pc.gray(`running in ${execDir}`));
278
+ }
268
279
  const concurrency = options.concurrency ?? getDefaultConcurrency();
269
280
  const failFast = !options.keepGoing;
270
281
  let allSucceeded = true;
@@ -275,23 +286,29 @@ async function executeLocal(options) {
275
286
  event,
276
287
  secrets,
277
288
  kiciDir,
289
+ execDir,
278
290
  concurrency,
279
291
  failFast
280
292
  };
281
- for (const workflow of matchedWorkflows) {
282
- const outcome = await runOneMatchedWorkflow(workflow, ctx);
283
- if (outcome === null) continue;
284
- workflowResults.push(outcome.result);
285
- if (!outcome.succeeded) allSucceeded = false;
286
- }
287
- if (options.json) console.log(formatLocalJsonResult(workflowResults));
288
- else if (options.junit) {
289
- const junitXml = formatLocalJunitResult(workflowResults);
290
- await writeFile(options.junit, junitXml);
291
- if (!isQuiet) logger.info(pc.green(`JUnit XML written to ${options.junit}`));
293
+ try {
294
+ for (const workflow of matchedWorkflows) {
295
+ const outcome = await runOneMatchedWorkflow(workflow, ctx);
296
+ if (outcome === null) continue;
297
+ workflowResults.push(outcome.result);
298
+ if (!outcome.succeeded) allSucceeded = false;
299
+ }
300
+ if (options.json) console.log(formatLocalJsonResult(workflowResults));
301
+ else if (options.junit) {
302
+ const junitXml = formatLocalJunitResult(workflowResults);
303
+ await writeFile(options.junit, junitXml);
304
+ if (!isQuiet) logger.info(pc.green(`JUnit XML written to ${options.junit}`));
305
+ }
306
+ if (!isQuiet) displayLocalSummary(workflowResults);
307
+ return allSucceeded;
308
+ } finally {
309
+ if (materialized) if (allSucceeded && !options.keep) await materialized.cleanup();
310
+ else logger.info(pc.gray(`kept isolated checkout at ${execDir}`));
292
311
  }
293
- if (!isQuiet) displayLocalSummary(workflowResults);
294
- return allSucceeded;
295
312
  }
296
313
  /**
297
314
  * Get default concurrency based on available CPU parallelism.
@@ -14,11 +14,14 @@ import type { ResolvedJob, LocalJobResult } from './types.js';
14
14
  /**
15
15
  * Context passed to executeResolvedJob for single-job execution.
16
16
  */
17
- interface JobExecutionContext {
17
+ export interface JobExecutionContext {
18
18
  workflowName: string;
19
19
  event: SimulatedEvent;
20
20
  secrets: ParsedSecrets;
21
+ /** Original `.kici/` dir — used for compile, secrets, and SDK setter resolution. */
21
22
  kiciDir: string;
23
+ /** Directory steps execute against (isolated tmp checkout, or the repo root with --in-place). */
24
+ execDir: string;
22
25
  jobOutputsMap: OutputsMap;
23
26
  signal: AbortSignal;
24
27
  }
@@ -36,5 +39,4 @@ export declare function resolveJobs(workflow: Workflow, event: SimulatedEvent):
36
39
  * check abort signal between steps, collect outputs.
37
40
  */
38
41
  export declare function executeResolvedJob(resolvedJob: ResolvedJob, context: JobExecutionContext): Promise<LocalJobResult>;
39
- export {};
40
42
  //# sourceMappingURL=job-runner.d.ts.map
@@ -215,7 +215,7 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
215
215
  sdkSetters.setJobOutputsMap(context.jobOutputsMap);
216
216
  setJobOutputsMap(context.jobOutputsMap);
217
217
  const hasMatrix = Object.keys(matrixValues).length > 0;
218
- const repoRoot = path.dirname(context.kiciDir);
218
+ const repoRoot = context.execDir;
219
219
  const stepCtx = createStepContext({ name: context.workflowName }, {
220
220
  name: expandedName,
221
221
  runsOn: typeof job.runsOn === "string" ? job.runsOn : Array.isArray(job.runsOn) ? job.runsOn.join(",") : (Array.isArray(job.runsOn.labels) ? job.runsOn.labels : [job.runsOn.labels]).join(",")