@kici-dev/compiler 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/commands/compile.js +1 -13
  3. package/dist/commands/preview.js +1 -8
  4. package/dist/commands/types.js +1 -2
  5. package/dist/errors/formatter.d.ts +2 -4
  6. package/dist/errors/formatter.js +1 -3
  7. package/dist/errors/index.d.ts +1 -1
  8. package/dist/errors/index.js +2 -2
  9. package/dist/generators/secrets-dts.d.ts +0 -1
  10. package/dist/generators/secrets-dts.js +1 -2
  11. package/dist/llm-context/llms-architecture.txt +27 -13
  12. package/dist/llm-context/llms-cli.txt +26 -6
  13. package/dist/llm-context/llms-features.txt +272 -91
  14. package/dist/llm-context/llms-full.txt +649 -235
  15. package/dist/llm-context/llms-getting-started.txt +20 -20
  16. package/dist/llm-context/llms-patterns.txt +10 -6
  17. package/dist/llm-context/llms-providers.txt +4 -6
  18. package/dist/llm-context/llms-sdk-runtime.txt +37 -36
  19. package/dist/llm-context/llms-sdk.txt +253 -57
  20. package/dist/llm-context/llms.txt +6 -6
  21. package/dist/local-plane/plane-manager.js +2 -2
  22. package/dist/lockfile/generator.js +137 -42
  23. package/dist/lockfile/index.d.ts +0 -2
  24. package/dist/lockfile/index.js +1 -2
  25. package/dist/templates/package-json.js +1 -1
  26. package/dist/test-runner/dry-run.d.ts +1 -2
  27. package/dist/test-runner/dry-run.js +1 -18
  28. package/dist/types.d.ts +32 -8
  29. package/dist/types.js +7 -1
  30. package/dist/validation/validator.js +40 -0
  31. package/package.json +6 -6
  32. package/sbom.spdx.json +126 -121
  33. package/dist/lockfile/purity-analyzer.d.ts +0 -25
  34. package/dist/lockfile/purity-analyzer.js +0 -204
  35. package/dist/lockfile/purity-diagnostics.d.ts +0 -31
  36. package/dist/lockfile/purity-diagnostics.js +0 -52
@@ -331,7 +331,7 @@ group: () => 'deploy';
331
331
  group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
332
332
  ```
333
333
 
334
- The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. This differs from job-level `concurrencyGroup` (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)), where the compiler performs purity analysis and can inline pure functions for orchestrator-side evaluation.
334
+ The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. Job-level `concurrencyGroup` functions (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)) resolve the same way on the agent's init step, never in the orchestrator.
335
335
 
336
336
  ## cancelInProgress mode
337
337
 
@@ -568,7 +568,7 @@ job('deploy-review', {
568
568
  });
569
569
  ```
570
570
 
571
- A pure function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is evaluated inline at dispatch with no init-job overhead. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
571
+ A dynamic context function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is resolved on the eval agent's init step before the job runs. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
572
572
 
573
573
  ### Multiple contexts per job
574
574
 
@@ -871,22 +871,17 @@ job('deploy', {
871
871
 
872
872
  ## How it works
873
873
 
874
- When you define a dynamic value as a function, the compiler analyzes it at compile time to determine whether it is **pure** (can be evaluated without cloning the repo or running an init job).
874
+ When you define a dynamic value as a function, it is resolved on the eval agent as a short **init** step that runs before the job:
875
875
 
876
- ### Pure functions (inline evaluation)
876
+ 1. The orchestrator dispatches a lightweight `__init__` job to an agent.
877
+ 2. The agent loads the compiled workflow bundle and calls your function with the normalized event.
878
+ 3. The agent reports the resolved values back to the orchestrator, which dispatches the real execution job with them applied.
877
879
 
878
- A pure function is one that:
880
+ This resolution appears in the run timeline as an `Init:` entry. The orchestrator never evaluates workflow code — every dynamic `context`, `env`, and `concurrencyGroup` function runs agent-side, whatever it references.
879
881
 
880
- - Is synchronous (no `async`/`await`)
881
- - Only references its parameters and local variables
882
- - Does not import or require external modules
883
- - Does not access globals like `process`, `fetch`, `console`, `setTimeout`, etc.
884
- - Uses only safe built-in constructors: `String`, `Number`, `Boolean`, `Array`, `Object`, `JSON`, `Math`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `encodeURIComponent`, `decodeURIComponent`, `encodeURI`, `decodeURI`
885
- - Does not use `this`, `new`, `class`, `throw`, `try`/`catch`, `delete`, `var`, `yield`, or mutation operators (`++`, `--`, `+=`, etc.)
882
+ `kici preview` lists the injected `__init__` job under each affected job, so you can spot it before the first run.
886
883
 
887
- When the compiler detects a pure function, it serializes the function source directly into the lock file as an inline expression. At dispatch time, the orchestrator evaluates the expression in a sandboxed VM context (~0ms overhead) instead of dispatching an init job.
888
-
889
- **Examples of pure functions:**
884
+ **Examples:**
890
885
 
891
886
  ```typescript
892
887
  // Simple branch extraction
@@ -898,65 +893,32 @@ env: (event) => ({ BRANCH: event.targetBranch });
898
893
  // Concatenation with event data
899
894
  concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
900
895
 
901
- // Using safe globals
902
- env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
903
-
904
- // Local variables are fine
896
+ // Local variables and safe globals
905
897
  context: (event) => {
906
898
  const parts = event.targetBranch.split('/');
907
899
  return parts[parts.length - 1];
908
900
  };
909
- ```
910
-
911
- ### Impure functions (init-job evaluation)
912
-
913
- If the compiler determines a function is impure, it prints a `warning [W101]` naming the affected field (`context`, `env`, or `concurrencyGroup`), the reason the function was judged impure, and the ~5-10 second init-job cost — and compilation still succeeds. The function falls back to the two-phase init model. This means:
914
-
915
- 1. The orchestrator dispatches a special `__init__` job to a builder agent
916
- 2. The builder agent clones the repository and evaluates the function
917
- 3. The resolved values are sent back to the orchestrator
918
- 4. The orchestrator dispatches the real execution job with the resolved values
919
901
 
920
- This adds approximately 5-10 seconds of overhead for cloning and evaluation.
921
-
922
- `kici preview` lists the injected `__init__` job under each affected job, so you can spot the init-job cost before the first run.
923
-
924
- **Examples of impure functions (will use init job):**
925
-
926
- ```typescript
927
- // Async functions cannot be inlined
902
+ // Async lookups, module access, and process/global reads all work
928
903
  context: async (event) => await lookupEnv(event.targetBranch);
929
-
930
- // External module references
931
- env: (event) => {
932
- const config = require('./config');
933
- return config.env;
934
- };
935
-
936
- // Process/global access
937
- context: (event) => process.env.DEFAULT_ENV || 'staging';
938
-
939
- // Dynamic imports
940
- env: async (event) => {
941
- const m = await import('./config.js');
942
- return m.default;
943
- };
904
+ env: (event) => ({ DEFAULT_ENV: process.env.DEFAULT_ENV ?? 'staging' });
944
905
  ```
945
906
 
946
- ## Performance comparison
907
+ ## Performance
908
+
909
+ | Value | Overhead | Example |
910
+ | ------------------------ | --------- | ---------------------------------------- |
911
+ | Static value | None | `context: 'staging'` |
912
+ | Dynamic value (function) | Init step | `context: (event) => event.targetBranch` |
947
913
 
948
- | Evaluation path | Overhead | When used |
949
- | ------------------------------------ | -------- | --------------------------------------------------------------- |
950
- | Static value (string/object literal) | ~0ms | `context: 'staging'` |
951
- | Inline expression (pure function) | ~0ms | `context: (event) => event.targetBranch` |
952
- | Init job (impure function) | ~5-10s | `context: async (event) => await lookupEnv(event.targetBranch)` |
914
+ A static value is baked into the lock file and needs no init step. A dynamic value always resolves through the agent's init step, so reach for a function only when the value genuinely depends on the event.
953
915
 
954
916
  ## Tips
955
917
 
956
- - **Write pure functions whenever possible** to avoid the init-job delay. Most context and env computations only need the event payload data.
957
- - **Check compiler warnings** -- the compiler prints a `warning [W101]` when a function is classified as impure, naming the reason and the ~5-10s init-job cost. Run `kici preview` to see the injected `__init__` job listed under each affected job before your first run.
958
- - **Runtime errors in inline expressions cause immediate job failure.** There is no fallback to the init-job path. If your pure function throws at runtime (e.g., accessing a property on `undefined`), the job fails immediately.
959
- - **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where pure vs. impure functions run relative to rules, hooks, and step bodies.
918
+ - **Prefer static values when you can.** Most context and env values are the same on every event; only make them dynamic when they truly depend on the event payload.
919
+ - **Run `kici preview`** to see the injected `__init__` job listed under each affected job before your first run.
920
+ - **A runtime error in a dynamic function fails the job.** If your function throws when the init step runs it (e.g., accessing a property on `undefined`), the job fails immediately.
921
+ - **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where dynamic values run relative to rules, hooks, and step bodies.
960
922
  - **The event parameter is the normalized event envelope** — the same shape rules receive as `ctx.event`: `{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }` (see the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/) for the complete schema). Narrow on `event.type` (`'push'`, `'pull_request'`, `'tag'`, …) to branch per trigger kind. The raw provider webhook body is nested at `event.payload` (for GitHub pushes: `payload.ref`, `payload.after`, `payload.repository`, …).
961
923
 
962
924
  ---
@@ -1493,35 +1455,151 @@ export default workflow('org-lint', {
1493
1455
  });
1494
1456
  ```
1495
1457
 
1496
- Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org.
1458
+ Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org, including one whose identifier starts with a dot (`.github/workflows-config`) — a repo identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own. Path globs in `paths:` keep the usual convention and do not match dot-prefixed files unless the pattern spells the dot out.
1497
1459
 
1498
1460
  ### At a dual-repo checkout
1499
1461
 
1500
- The agent receives two sets of context during a global workflow execution:
1462
+ The agent checks out both repos. **Inside a step body**, `env` carries a pointer to each working tree:
1501
1463
 
1502
1464
  | `env` var | Points to |
1503
1465
  | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
1504
1466
  | `KICI_SOURCE_REPO_PATH` | The **source** repo's working tree (the repo that emitted the event). This is the repo the job's `$` / `git` commands operate on by default. |
1505
1467
  | `KICI_WORKFLOW_REPO_PATH` | The **workflow** repo's working tree (the repo that authored the workflow). Useful for reading shared scripts or config from your CI repo. |
1506
1468
 
1507
- Source repo secrets are **not** available to a global workflow's job by default see _Elevated access_ below.
1469
+ Both variables are step-body context. They are **not** projected into the job's process environment, so a job-level `env:` block, a container image's entrypoint, or a shell command outside a step body will not see them. Outside a step body, use the `sourceRepo` / `workflowRepo` pair on the filter, generator, and rule contexts described below.
1470
+
1471
+ They are also set **only when there are two repos to point at**. An event from the workflow's own repo is matched from that repo's lock file rather than as a global candidate, so the workflow runs as an ordinary single-repo workflow: one checkout, and neither variable set. Read them with a fallback, as the example above does.
1472
+
1473
+ A global workflow's job runs with **no secrets at all** — neither the source repo's nor its own. See _Secrets are not available_ below.
1474
+
1475
+ ### The triggering event
1476
+
1477
+ `ctx.event` inside a global workflow's job is the **source** repo's normalized event — the push or PR that fired the workflow, from a repo the workflow's own author may not own. `ctx.event.sourceRepo` names that repo.
1478
+
1479
+ That field is what makes a per-source-repo concurrency group expressible — and you have to write it. A global workflow runs on events from many repos, and their default branches share a name, so a group keyed on the branch alone puts every repo in one group, and with `cancelInProgress` (the default) one repo's push cancels another repo's in-flight run. That is still the behaviour of a branch-only group; naming the source repo in the key is what separates them:
1480
+
1481
+ ```ts
1482
+ concurrency: {
1483
+ group: ({ branch, event }) => `${event.sourceRepo}:${branch}`,
1484
+ cancelInProgress: true,
1485
+ },
1486
+ ```
1487
+
1488
+ ### Narrowing to the repos that need it
1489
+
1490
+ A global workflow that matches `myorg/*` will, by default, run on every repo in the org. Three mechanisms narrow it to the repos it actually applies to, in increasing order of power:
1491
+
1492
+ 1. **A `requires` content filter on the trigger** — the cheapest gate. The orchestrator checks a file's contents (a JSON-path probe over `package.json`, for example) and drops the workflow **before any agent is dispatched** when the condition is not met. See [`requires` on triggers](https://docs.kici.dev/user/sdk/triggers/#content-requirements-requires). This is provider-dependent — it needs a file-contents fetcher, which the GitHub provider supplies.
1493
+ 2. **A workflow-level `filter` predicate** — arbitrary TypeScript over the checked-out source tree (below). Works with any provider that clones.
1494
+ 3. **A `DynamicJobFn`** — generate the exact job set from the source repo's state ([Generating jobs per source repo](https://docs.kici.dev/user/global-workflows/#generating-jobs-per-source-repo) below).
1495
+
1496
+ ### Narrowing with a filter
1497
+
1498
+ Before reaching for a `filter`, check whether a declarative filter answers the question. `commitMessage` (on the trigger) and `requires` (over source files) are evaluated by the orchestrator from data it already has, so they cost no evaluation job at all — while a `filter` predicate dispatches one per (event × workflow repo). Gating on a `[skip ci]` marker, a conventional-commit prefix, or the contents of a named config file needs no predicate.
1499
+
1500
+ A workflow can declare a `filter`: a predicate that decides whether the workflow applies to this event at all.
1501
+
1502
+ ```ts
1503
+ import { workflow, job, step, push } from '@kici-dev/sdk';
1504
+
1505
+ export default workflow('org-container-lint', {
1506
+ on: [push({ repos: ['myorg/*'] })],
1507
+ filter: async ({ sourceRepo, changedFilesStatus, $ }) => {
1508
+ // `changedFiles` throws when the diff is unavailable, so guard first.
1509
+ if (changedFilesStatus !== 'fetched') return true;
1510
+ const found = await $`ls ${sourceRepo.path}`;
1511
+ return found.stdout.includes('Dockerfile');
1512
+ },
1513
+ jobs: [
1514
+ job('lint-dockerfile', {
1515
+ runsOn: ['kici:os:linux'],
1516
+ steps: [
1517
+ step('lint', async ({ $, env }) => $`hadolint ${env.KICI_SOURCE_REPO_PATH}/Dockerfile`),
1518
+ ],
1519
+ }),
1520
+ ],
1521
+ });
1522
+ ```
1523
+
1524
+ The filter receives a `FilterContext`:
1525
+
1526
+ | Property | Type | Description |
1527
+ | -------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------ |
1528
+ | `sourceRepo` | `RepoInfo` | The repo whose event triggered this evaluation, checked out on the evaluating agent. |
1529
+ | `workflowRepo` | `RepoInfo` | The repo that registered the workflow. Identical to `sourceRepo` for a same-repo workflow. |
1530
+ | `event` | `EventPayload` | The normalized event envelope. |
1531
+ | `changedFiles` | `string[]` | Files changed in this event. Throws when unavailable — guard with `changedFilesStatus`. |
1532
+ | `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` can be read. |
1533
+ | `env` | `Record<string, string\|undefined>` | Environment variables. |
1534
+ | `$` | zx shell | Shell executor. |
1535
+
1536
+ `RepoInfo` carries `path` (an absolute path to the checkout on the evaluating agent) plus optional `ref` and `sha`. **Both are optional** — an event that carries no single ref leaves them undefined, so guard before reading them.
1537
+
1538
+ **`sourceRepo.path` is not stable across evaluations.** Its _contents_ are: the evaluating agent and the later run see the same tree at the same commit. The path itself is not — a different working directory, and possibly a different machine. Read _through_ it; never embed it in a job name, an output, or anything compared across calls.
1539
+
1540
+ **A `filter` must be pure and deterministic.** Decide from the context alone — the event, the changed files, and the checked-out tree — so the same event always yields the same verdict.
1541
+
1542
+ ### Global and same-repo filters differ
1543
+
1544
+ The same `filter` keyword means two different things depending on whether the workflow is global:
1545
+
1546
+ | | Global workflow (`repos:` on a trigger) | Same-repo workflow |
1547
+ | ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
1548
+ | Evaluated | once per (event × workflow repo) | once per job that reaches dispatch, and once per job generator |
1549
+ | Evaluated relative to the run | **before** any run row exists | **after** the run row exists |
1550
+ | A `false` verdict leaves | no run at all — nothing appears in the dashboard | a run whose only entries are the evaluation jobs, rolling up to `success` |
1551
+ | `sourceRepo` vs `workflowRepo` | two different repos | the same repo |
1552
+
1553
+ Two consequences of the same-repo shape are worth designing for. A workflow with ten jobs calls its filter ten times for one event — each on its own agent with its own checkout and its own `$` — so anything the predicate does happens that many times: keep it cheap and side-effect free. And if the predicate can answer differently for the same event, the workflow will _partially_ dispatch, running some jobs and not others.
1554
+
1555
+ **A held or rejected job is not filtered at all.** A job held for approval, or rejected by a context rule, already has a gate — the hold or the rule — so it never takes a filter verdict, and an approved job dispatches without one. Concretely: a path filter cannot stop an approval request for a job the change does not concern.
1556
+
1557
+ ### Generating jobs per source repo
1558
+
1559
+ A global workflow's job generators run in the same pre-run evaluation as the filter, with both repos on disk. `sourceRepo` and `workflowRepo` are on the generator context, so one workflow repo can produce a different job set per source repo:
1560
+
1561
+ ```ts
1562
+ import { job, step, workflow, push, type DynamicJobFn } from '@kici-dev/sdk';
1563
+ import { readFile } from 'node:fs/promises';
1564
+
1565
+ const perRepoJobs: DynamicJobFn = async ({ sourceRepo }) => {
1566
+ if (!sourceRepo) return [];
1567
+ const pkg = JSON.parse(await readFile(`${sourceRepo.path}/package.json`, 'utf8'));
1568
+ return Object.keys(pkg.scripts ?? {})
1569
+ .filter((s) => s.startsWith('ci:'))
1570
+ .map((s) =>
1571
+ job(s.replace(':', '-'), {
1572
+ runsOn: ['kici:os:linux'],
1573
+ steps: [step('run', async ({ $ }) => $`pnpm ${s}`)],
1574
+ }),
1575
+ );
1576
+ };
1577
+
1578
+ export default workflow('org-ci', {
1579
+ on: [push({ repos: ['myorg/*'] })],
1580
+ jobs: [perRepoJobs],
1581
+ });
1582
+ ```
1583
+
1584
+ The same `sourceRepo.path` caution applies: read the tree through it, and derive job names from the repo's _contents_, never from the path.
1508
1585
 
1509
1586
  ## Enabling global workflows
1510
1587
 
1511
- Global workflows are **opt-in per org**. In a fresh org, `repos:`-bearing workflows are registered but never dispatched.
1588
+ Global workflows are gated by a **fleet-wide master switch** held by the orchestrator operator, off by default. Until it is on, `repos:`-bearing workflows are registered but never dispatched.
1512
1589
 
1513
- 1. Open the dashboard **Settings → Global workflows**.
1514
- 2. Turn on **Enable global workflows** (the master toggle). This is the kill-switch every other toggle below is ignored while this is off.
1515
- 3. Decide which authoring/source controls you need:
1590
+ 1. **The operator enables it cluster-wide** with `kici-admin cluster-settings set --global-workflows-enabled true`. This is the kill-switch — every per-org control below is ignored while it is off, and it cannot be flipped from the dashboard. The dashboard's **Settings → Global workflows** tab shows its current state as a read-only badge.
1591
+ 2. In the dashboard → **Settings Global workflows**, decide which authoring/source controls you need. These per-org lists stay dashboard-editable; an org that has set none means "no per-org restrictions", not a denial.
1516
1592
 
1517
- | Setting | What it controls | Typical use |
1518
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
1519
- | Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
1520
- | Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
1521
- | Elevated access | Authoring repos listed here get **read access to source-repo secrets** during execution. Globs matched against the authoring repo identifier. | A `myorg/ci-deploy` repo that needs to read a source repo's `NPM_TOKEN` to publish releases. |
1593
+ | Setting | What it controls | Typical use |
1594
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
1595
+ | Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
1596
+ | Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
1597
+ | Elevated access | **Deprecated and not enforced.** Stored and echoed back, but nothing reads it — a global workflow's job receives no secrets, so there is no access for it to grant. See _Secrets are not available_. | None. Clear the list so it does not imply a grant that is not in force. |
1522
1598
 
1523
1599
  All three lists accept globs. Leading `!` inside a single pattern is not supported here; negation is via the list-is-implicit-deny semantics, so keep it simple (`myorg/ci-*`, `myorg/platform-*`).
1524
1600
 
1601
+ Patterns match repo identifiers by the same rule as `repos:` on a trigger: an identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own and a wildcard segment matches one. `myorg/*` covers `myorg/.github`, and `**` covers every repo in the org. Review any existing entry that relies on a wildcard to reach — or to spare — a dot-prefixed repo name.
1602
+
1525
1603
  ### Saving and reverting
1526
1604
 
1527
1605
  The page is a two-state editor — changes are local until you click **Save changes**, and you can abandon them with **Discard changes**. There is no partial save; the PATCH is all-or-nothing per save click.
@@ -1533,17 +1611,23 @@ The page is a two-state editor — changes are local until you click **Save chan
1533
1611
  A global workflow fires only if:
1534
1612
 
1535
1613
  1. **The authoring repo is allowed.** If _Allowed author repos_ is ON, the workflow's authoring repo must match at least one allow-list glob. If OFF, any repo may author. Enforced at two points:
1536
- - At registration time (extraction from the lock file — non-matching globals are dropped with a warning).
1614
+ - At registration time (extraction from the lock file — non-matching globals are dropped, and the orchestrator logs `Global workflows excluded from registration` naming each one).
1537
1615
  - At dispatch time (defense-in-depth — policy changes after registration still take effect).
1538
1616
  2. **The source repo is not denied.** If the event's source repo matches any glob in _Blocked source repos_, the global workflow is skipped. Enforced at dispatch time.
1539
1617
 
1540
- Both checks are logged to the orchestrator. Grep the logs for `Skipping global workflow` to see enforcement in action.
1618
+ Both checks are logged to the orchestrator. Grep for `Global workflows excluded from registration` (registration time) and `Skipping global workflow dispatch` (dispatch time) to see enforcement in action.
1541
1619
 
1542
- ### Elevated access (source-repo secrets)
1620
+ Both checks read the settings of the organization the **event's source** resolves to. If no webhook source maps the event's routing key to an organization, the orchestrator resolves the built-in `__default__` organization anchor instead — and since nobody has enabled global workflows for that anchor, every global workflow is refused. The registration log line carries the organization it decided against plus the remedy, so this case is distinguishable from a real opt-in that is simply switched off. See the troubleshooting table below.
1543
1621
 
1544
- By default a global workflow's job runs with credentials scoped to the **workflow** repo — it can clone both repos but cannot read the source repo's scoped secrets. That's the safe default: a random workflow in `myorg/ci-pipelines` does not get read access to secrets in `myorg/backend` just because it runs on a push there.
1622
+ ### Secrets are not available
1545
1623
 
1546
- Adding the authoring repo to the _Elevated access_ list flips that: the job receives the source repo's secret context, so deploy and release flows that need `NPM_TOKEN` / `AWS_ROLE_ARN` / etc. from the source repo can read them. Treat elevated repos as effective owners of every source repo's CI secrets only add repos you fully trust.
1624
+ A global workflow's job is dispatched with **no secret material** not the source repo's, and not the workflow repo's own. The organization-wide dispatch path binds no secret contexts, so a `contexts:` declaration on a global workflow resolves to nothing and any secret the steps expect is simply absent. Plan for it: a global workflow is for checks, policy and reporting that need only the two checkouts, not for deploys that need credentials.
1625
+
1626
+ This is about your **stored secrets**, not about repository access: the job is still handed a short-lived clone token for each repo it checks out, which is how the dual checkout works at all. What it does not get is anything from a secret context.
1627
+
1628
+ To run something that needs secrets on a source repo's event, put those jobs in a per-repository workflow in that repo, where the workflow's `contexts:` resolve normally.
1629
+
1630
+ The **Elevated access** setting reads as the way to lift this, and it is not: it is **deprecated and never consulted**. Nothing in the dispatch path reads the list, and adding a repo to it does not make any secret readable. It is kept only so an existing value stays visible and clearable, and is removed at the next major version — see [Deprecations](https://docs.kici.dev/user/deprecations/).
1547
1631
 
1548
1632
  ## When does it fire?
1549
1633
 
@@ -1551,14 +1635,111 @@ Same-repo globals (a workflow in `myorg/app` with `repos: ['myorg/app']`) fire o
1551
1635
 
1552
1636
  Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workflowRun()`, etc. all accept `repos:`. `kiciEvent()` / `schedule()` / cron-like triggers have no source repo, so they're always per-org-registered regardless of `repos:`.
1553
1637
 
1638
+ A global workflow that declares a `filter` or a job generator is decided by one **evaluation job per (event × workflow repo)**, dispatched before any run exists. That job checks out both repos once and evaluates every candidate workflow from that repo, so ten global workflows in one CI repo cost one evaluation, not ten.
1639
+
1640
+ When that evaluation cannot reach a verdict — it fails, breaches its budget, or never reports — the workflows it was deciding on **do not run**. On a provider that supports commit checks, that posts a `failure` check named **`KiCI: Organization workflow evaluation`** on the source commit, so the outcome is visible instead of silent. Three things to know about it:
1641
+
1642
+ - The check is posted whether the evaluation failed **outright** or only **partly**. A per-workflow budget breach, or a `filter` that throws, leaves that one workflow undecided while its neighbours from the same repo are decided and run normally; the check then names only the undecided ones. So a broken `filter` is reported the same way whether or not other global workflows happen to share its repo.
1643
+ - Branch protection that lists required checks by name is unaffected, because the check is not on that list. Merge automation that requires _every_ check to be green will block on it.
1644
+ - **The check clears only on a new commit.** A provider redelivery of the same event is dropped as a duplicate, so re-delivering the webhook will not re-run the evaluation.
1645
+
1646
+ ## Approval gates are not supported
1647
+
1648
+ A global workflow cannot carry an `approval` gate, at the workflow level or on a job. Approval holds are applied by the per-repository dispatch path; the global path dispatches its jobs without consulting one, so a gate declared here would never be enforced. `kici compile` refuses it with `error [E124]` rather than accepting a security control the workflow does not actually have. A job produced by a `dynamicJob` generator never passes through the compiler, so that case is caught at dispatch instead — the orchestrator logs an error naming the workflow and job, and runs it ungated.
1649
+
1650
+ To gate a deployment behind a human, put the gated jobs in a workflow whose triggers carry no `repos:`.
1651
+
1652
+ ## Re-running is not supported
1653
+
1654
+ A global workflow's run cannot be re-run. The rerun path resolves a workflow out of the repo the run acted on, and for a global workflow that is the **source** repo — not the workflow repo that declares it. Re-running is refused with an error naming both repos rather than resolving the wrong one, which for a source repo carrying a same-named workflow would silently run that workflow instead, with the source repo's credentials and none of the global job configuration.
1655
+
1656
+ To run it again, push a new commit to the source repo (a provider redelivery of the same event is dropped as a duplicate), or trigger it from the workflow repo.
1657
+
1658
+ ## Requirements a filter places on the run
1659
+
1660
+ A `filter` reads the source tree, so the evaluation must be able to obtain one. A job that restores its workflow source from the cache and has no source repository to clone from fails with an explicit error rather than evaluating the filter against an empty tree. This applies to dispatch paths that run without a source repository configured — a filter and such a path are mutually exclusive; drop one or the other.
1661
+
1554
1662
  ## Troubleshooting
1555
1663
 
1556
- | Symptom | Likely cause | Where to look |
1557
- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
1558
- | Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` / `Skipping global workflow registration: not permitted` |
1559
- | `repos:` has no effectworkflow only fires on its own repo | Master toggle OFF. Without opt-in, the orchestrator treats the workflow as per-repo-only. | Dashboard Settings Global workflows (top toggle) |
1560
- | Source repo secrets unavailable in a global job | Expected default elevate the authoring repo to grant access. | Dashboard → Settings → Global workflows _Elevated access_ |
1561
- | Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
1664
+ | Symptom | Likely cause | Where to look |
1665
+ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1666
+ | Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` (dispatch time) / `Global workflows excluded from registration` (registration time) |
1667
+ | A global workflow is never registered at all it is absent from `kici-admin registration list` | The push that should have registered it resolved to the `__default__` organization anchor, because no webhook source maps its routing key to an organization, or the fleet-wide master switch is off. | Orchestrator log: `Global workflows excluded from registration` with `"orgId": "__default__"`. Its `remedy` field names both fixes — map the source (`kici-admin source update <routingKey> --customer-id <org>`) and enable global workflows cluster-wide if it is not already (`kici-admin cluster-settings set --global-workflows-enabled true`). |
1668
+ | `repos:` has no effect workflow only fires on its own repo | The fleet-wide master switch is off. Without it, the orchestrator treats the workflow as per-repo-only. | Check the fleet-wide switch with `kici-admin cluster-settings show`. The dashboard → Settings → Global workflows tab shows it as a read-only badge. |
1669
+ | Secrets unavailable in a global job | Expected a global workflow's job receives no secrets at all, and the _Elevated access_ list is not enforced. | Move the jobs that need credentials into a per-repository workflow in the repo that owns the secrets |
1670
+ | Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
1671
+ | Global workflow registered, enabled, allowed — and still no run appears | Its `filter` returned `false`. A global filter runs before the run is created, so a suppressed workflow leaves nothing behind at all. | [Reading a global workflow's filter output](https://docs.kici.dev/user/global-workflows/#reading-a-global-workflows-filter-output) — the evaluation round's own log. The orchestrator also logs `Global workflow skipped by eval round`, naming the workflow and the reason. |
1672
+ | Global workflow never fires for one particular source repo | Its `repos:` patterns do not match that repo's identifier. | Orchestrator log: `Global workflows dropped by their repos filter` — one line per delivery, naming each dropped workflow, its repo and its patterns. |
1673
+ | A `failure` check named `KiCI: Organization workflow evaluation` on a commit | The pre-run evaluation failed or timed out, so the global workflows from that repo were not run. | Orchestrator log for the evaluation job; push a new commit to re-evaluate (a redelivery is dropped as a duplicate). |
1674
+ | Same-repo workflow shows a `success` run with no jobs in it | Its `filter` returned `false`. A same-repo filter runs after the run exists, so the run remains, carrying only the evaluation jobs. | The run detail page — the evaluation job's log records the filter verdict. |
1675
+ | Re-run is refused with "Cannot re-run an organization-wide workflow" | Expected — the run executed against a source repo that does not declare the workflow. | [Re-running is not supported](https://docs.kici.dev/user/global-workflows/#re-running-is-not-supported) — push a new commit to the source repo instead. |
1676
+ | Every global workflow stopped running right after an orchestrator upgrade | The agents were not upgraded first. An agent older than v0.5.0 cannot evaluate a global workflow, and one containing a `dynamicJob` now needs an evaluation even without a `filter` — so its **static** jobs stop too. | The `KiCI: Organization workflow evaluation` check names the agent versions it found. Upgrade every `kici:role:init-runner` agent to v0.5.0 or newer. |
1677
+
1678
+ ### Reading a global run in the dashboard
1679
+
1680
+ A global run is attributed to the **source** repo — the repo whose event
1681
+ triggered it, and whose code the jobs check out. Its run detail page names both
1682
+ repos, so you can tell it apart from an ordinary per-repo run:
1683
+
1684
+ | Row | Shows |
1685
+ | ------------ | ------------------------------------------------------------------------ |
1686
+ | `Repository` | the source repo — the one the run acted on |
1687
+ | `Defined in` | the workflow repo, tagged `Organization-wide`. Absent on an ordinary run |
1688
+ | `Workflow` | links into the **workflow** repo, on its default branch |
1689
+
1690
+ The `Workflow` link points at the workflow repo's default branch rather than at
1691
+ a commit: the run's own commit belongs to the source repo, and nothing records
1692
+ which commit of the workflow repo a given run used. So the link always shows the
1693
+ file as it stands now, which may have changed since the run.
1694
+
1695
+ The `Payload` tab shows the source repo's event — the webhook delivery the
1696
+ workflow reacted to, which for a global workflow comes from a repo you may not
1697
+ own. A global run dispatched before your orchestrator stored payloads for this
1698
+ path has none, and its tab reports that it could not load one.
1699
+
1700
+ #### Who can see it
1701
+
1702
+ A global run belongs to **both** repos, so a member whose role is scoped to
1703
+ either one reaches it — the team whose push triggered it, and the team that
1704
+ authored the workflow. Both see it in the run list, in the repository filter
1705
+ (which offers both names), and on the run detail page. Cancelling follows the
1706
+ same rule, so the team whose workflow is running can always stop it.
1707
+
1708
+ Releasing a **held** run is the one exception: approving a hold permits code to
1709
+ run against the source repo, so it stays with a member scoped to that repo. A
1710
+ member scoped only to the workflow repo sees the run but not its hold.
1711
+
1712
+ This applies only where the two repos genuinely differ. An ordinary per-repo run
1713
+ records no separate workflow repo and is scoped to its own repo exactly as
1714
+ before, and a member scoped to neither repo sees nothing in either case.
1715
+
1716
+ ### Reading a global workflow's filter output
1717
+
1718
+ A global workflow's `filter` runs in a pre-run evaluation round, and that round
1719
+ decides whether a run exists at all — so on the path where it suppresses a
1720
+ workflow there is no run, and nothing appears in the dashboard. The round's own
1721
+ log is still recorded. Read it with the orchestrator admin CLI, in two steps:
1722
+
1723
+ ```bash
1724
+ # 1. Find the round. Its workflow name is __globaleval__<owner>/<repo> of the
1725
+ # WORKFLOW repo. In the JSON rows, `id` is the job id and `run_id` is the
1726
+ # run id.
1727
+ kici-admin queue list --workflow-name '__globaleval__myorg/ci-pipelines' --limit 5 --json
1728
+
1729
+ # 2. Print the round's log (step 0 is the evaluation itself).
1730
+ kici-admin runs logs <run_id> --job <id>
1731
+ ```
1732
+
1733
+ Use `--json` on the first command: the plain table abbreviates both ids to their
1734
+ first eight characters, and the second command needs them in full.
1735
+
1736
+ The two steps need different permissions, so run both with an **owner or admin**
1737
+ token. Step 1 reads the dispatch queue, which requires `secret.read` — an auditor
1738
+ token is refused with a 403 and never reaches step 2. Step 2 requires only
1739
+ `run.read`, which every role carries.
1740
+
1741
+ Anything your `filter` writes with `console.log` appears there, alongside the
1742
+ per-candidate verdicts the round recorded.
1562
1743
 
1563
1744
  ## See also
1564
1745
 
@@ -1744,7 +1925,7 @@ export default workflow('build', {
1744
1925
 
1745
1926
  Per-field rules:
1746
1927
 
1747
- - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/kici-admin-cli#allow-http-npm--permit-non-https-private-npm-registries)).
1928
+ - **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#allow-http-npm--permit-non-https-private-npm-registries)).
1748
1929
  - **`scope`** — Optional. When present, the registry serves only that scope (`@my-org`). When absent, this entry becomes the **default** registry — at most one entry may omit `scope`.
1749
1930
  - **`tokenSecret`** — Mandatory `<context>:<secret-name>`. The orchestrator looks up the secret in the named context via the per-context secret resolver. The bare name **must not** contain a colon.
1750
1931
  - **`alwaysAuth`** — Defaults to `true`. Forces npm to send the token on every request (even GETs), which is what most managed-registry providers require.
@@ -1958,7 +2139,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
1958
2139
 
1959
2140
  - [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<context>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
1960
2141
  - [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
1961
- - [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/kici-admin-cli#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
2142
+ - [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
1962
2143
 
1963
2144
  ---
1964
2145
 
@@ -2264,7 +2445,7 @@ KiCI provides an explicit secrets API that gives workflow steps controlled acces
2264
2445
 
2265
2446
  ## Overview
2266
2447
 
2267
- Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
2448
+ Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration/) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
2268
2449
 
2269
2450
  This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
2270
2451
 
@@ -2285,13 +2466,13 @@ Use whichever fits the workflow — most small teams stay on the dashboard; ops
2285
2466
 
2286
2467
  ### When the operator has disabled dashboard writes
2287
2468
 
2288
- The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy). When that flip is on:
2469
+ The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy/). When that flip is on:
2289
2470
 
2290
2471
  - The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Hovering or keyboard-focusing the lock shows a tooltip with the exact `kici-admin secret set` invocation needed. The control itself is inert, so there is nothing to click.
2291
2472
  - The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
2292
2473
  - `kici-admin secret set` becomes the single entry point for new and updated secret values.
2293
2474
 
2294
- This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, environment bindings).
2475
+ This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, context bindings).
2295
2476
 
2296
2477
  ### CLI input modes
2297
2478
 
@@ -2326,7 +2507,7 @@ Two cross-cutting flags help every mode:
2326
2507
 
2327
2508
  `kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
2328
2509
 
2329
- A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy#cli-input-modes-for-the-plaintext-path).
2510
+ A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy/#cli-input-modes-for-the-plaintext-path).
2330
2511
 
2331
2512
  ## Accessing secrets
2332
2513
 
@@ -2561,6 +2742,6 @@ Note that `get()` is async -- you must `await` the result.
2561
2742
 
2562
2743
  When you run `kici types`, the compiler generates a `.kici/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
2563
2744
 
2564
- See [CLI reference](https://docs.kici.dev/user/cli) for the `kici types` command.
2745
+ See [CLI reference](https://docs.kici.dev/user/cli/authoring-and-local/#kici-types) for the `kici types` command.
2565
2746
 
2566
2747
  ---