@kici-dev/compiler 0.6.1 → 0.8.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 (57) hide show
  1. package/dist/cli.js +2 -2
  2. package/dist/commands/compile.js +5 -1
  3. package/dist/commands/doctor.js +8 -2
  4. package/dist/commands/init.d.ts +9 -0
  5. package/dist/commands/init.js +77 -12
  6. package/dist/commands/local.d.ts +9 -2
  7. package/dist/commands/local.js +14 -4
  8. package/dist/commands/preview.js +1 -1
  9. package/dist/commands/report/identity.d.ts +11 -0
  10. package/dist/commands/report/identity.js +7 -2
  11. package/dist/commands/run-routed.js +4 -0
  12. package/dist/commands/run.js +5 -2
  13. package/dist/commands/runs/logs.js +3 -2
  14. package/dist/commands/types.d.ts +6 -1
  15. package/dist/commands/types.js +2 -1
  16. package/dist/execution/executor.js +7 -1
  17. package/dist/llm-context/llms-architecture.txt +73 -87
  18. package/dist/llm-context/llms-cli-remote.txt +53 -8
  19. package/dist/llm-context/llms-cli.txt +71 -36
  20. package/dist/llm-context/llms-features-execution.txt +52 -6
  21. package/dist/llm-context/llms-features.txt +137 -6
  22. package/dist/llm-context/llms-full.txt +558 -180
  23. package/dist/llm-context/llms-getting-started.txt +5 -5
  24. package/dist/llm-context/llms-patterns.txt +81 -5
  25. package/dist/llm-context/llms-providers.txt +6 -2
  26. package/dist/llm-context/llms-sdk-runtime.txt +33 -18
  27. package/dist/llm-context/llms-sdk.txt +47 -7
  28. package/dist/llm-context/llms.txt +8 -8
  29. package/dist/local-plane/orchestrator-process.d.ts +0 -8
  30. package/dist/local-plane/orchestrator-process.js +6 -14
  31. package/dist/local-plane/paths.d.ts +1 -0
  32. package/dist/local-plane/paths.js +1 -0
  33. package/dist/local-plane/plane-log.d.ts +27 -0
  34. package/dist/local-plane/plane-log.js +39 -0
  35. package/dist/local-plane/plane-manager.js +2 -2
  36. package/dist/local-plane/plane-trigger.d.ts +28 -0
  37. package/dist/local-plane/plane-trigger.js +57 -2
  38. package/dist/local-plane/postgres.js +9 -6
  39. package/dist/local-plane/run-follow.js +2 -1
  40. package/dist/lockfile/generator.js +25 -9
  41. package/dist/lockfile/hasher.d.ts +5 -13
  42. package/dist/lockfile/hasher.js +1 -15
  43. package/dist/lockfile/workspace-siblings.d.ts +46 -0
  44. package/dist/lockfile/workspace-siblings.js +197 -0
  45. package/dist/remote/output/streaming.d.ts +12 -0
  46. package/dist/remote/output/streaming.js +20 -1
  47. package/dist/remote/platform-client.d.ts +2 -0
  48. package/dist/templates/package-json.d.ts +9 -7
  49. package/dist/templates/package-json.js +11 -9
  50. package/dist/test-runner/job-executor.js +1 -1
  51. package/dist/test-runner/rule-evaluator.js +1 -1
  52. package/dist/types.d.ts +6 -1
  53. package/package.json +7 -9
  54. package/sbom.spdx.json +123 -123
  55. package/dist/postinstall.d.ts +0 -9
  56. package/dist/postinstall.js +0 -62
  57. package/hack/postinstall.mjs +0 -105
@@ -232,7 +232,7 @@ triggered, or the repo had no lock file at that commit.
232
232
  source-registration mismatch).
233
233
  2. **Did anything match?** Run `kici preview push --branch <your-branch>`
234
234
  against your workflow. If it reports no matching workflow, your triggers don't
235
- cover that event/branch — the push was delivered and simply matched nothing.
235
+ cover that event/branch — the push was delivered and matched nothing.
236
236
  3. **Was there a lock file?** A repository with **no** `kici.lock.json` at the
237
237
  pushed commit produces no run and is not an error. Confirm the lock file is
238
238
  committed and current (see [Lock-file drift](https://docs.kici.dev/user/common-failures/#lock-file-drift)).
@@ -551,7 +551,7 @@ KiCI uses a **two-artifact model**: TypeScript workflows are the source of truth
551
551
  ## Why the lock file matters
552
552
 
553
553
  - **Orchestrator** fetches the lock file at the commit SHA and uses it to evaluate triggers and to look up the cached `.kici/` source tarball + `node_modules` tarball. It never runs your TypeScript.
554
- - **Agents** download the cached source tarball (or, on cold cache, the build agent clones + packs it), register the shared TypeScript loader hook, and dynamic-`import()` the workflow `.ts` directly. The lock file's per-workflow `contentHash` identifies the expected raw-source bytes and is verified against the extracted source before any step runs.
554
+ - **Agents** download the cached source tarball (or, on cold cache, the build agent clones + packs it), register the shared TypeScript loader hook, and dynamic-`import()` the workflow `.ts` directly. The lock file's per-workflow `contentHash` identifies the expected contents of the whole `.kici/` directory and is verified against the extracted source before any step runs. The tarball's own bytes are verified against the digest the orchestrator dispatched, and the restored tree **replaces** `.kici/` rather than being unpacked over it, so a file you deleted does not survive a cache hit.
555
555
 
556
556
  If you change a workflow file (`.ts`) but do **not** regenerate and commit the lock file, the repo at that commit has **drift**: the lock file no longer matches the source. Triggers and cache keys can be wrong, and runs can fail with a clear “stale lock file” error once the agent verifies the hash.
557
557
 
@@ -559,37 +559,38 @@ If you change a workflow file (`.ts`) but do **not** regenerate and commit the l
559
559
 
560
560
  The lock file (`kici.lock.json`) is a JSON file with the following top-level fields:
561
561
 
562
- | Field | Description |
563
- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
564
- | `schemaVersion` | Lock file schema version, stamped by the compiler that produced the lock. Incremented on every format change. The orchestrator accepts a range of versions — see [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window) — rather than requiring an exact match. |
565
- | `minReaderVersion` | The oldest orchestrator schema version that can read this lock (the newest breaking version at compile time). An orchestrator whose own schema is below this rejects the lock and asks you to upgrade it. Omitted on locks compiled before the compatibility window existed. See [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window). |
566
- | `source` | Reference to the source file and export (e.g., `{ file: '.kici/workflows/ci.ts', export: '#default' }`). |
567
- | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
568
- | `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
569
- | `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
562
+ | Field | Description |
563
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
564
+ | `schemaVersion` | Lock file schema version, stamped by the compiler that produced the lock. Incremented on every format change. The orchestrator accepts a range of versions — see [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window) — rather than requiring an exact match. |
565
+ | `minReaderVersion` | The oldest orchestrator schema version that can read this lock (the newest breaking version at compile time). An orchestrator whose own schema is below this rejects the lock and asks you to upgrade it. Omitted on locks compiled before the compatibility window existed. See [schema compatibility window](https://docs.kici.dev/user/lock-file-and-drift/#schema-compatibility-window). |
566
+ | `source` | Reference to the source file and export (e.g., `{ file: '.kici/workflows/ci.ts', export: '#default' }`). |
567
+ | `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
568
+ | `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
569
+ | `siblingsDigest` | SHA-256 over the git-tracked source of every in-repo `workspace:` / `file:` / `link:` / `portal:` sibling package `.kici` depends on, transitively. Part of the dependency cache key alongside `lockfileHash`, because editing a sibling's source moves no package manager lockfile. Omitted when `.kici` depends on no in-repo package, which is the common case. |
570
+ | `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
570
571
 
571
572
  Each workflow entry includes:
572
573
 
573
- | Field | Description |
574
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
575
- | `name` | Workflow name. |
576
- | `source` | Per-workflow source file and export reference. |
577
- | `contentHash` | SHA-256 of the raw workflow source mixed with `compileSchemaVersion` (and an `assetDigest` of declared `hashFiles` when present): `SHA-256(compileSchemaVersion + ":" + rawSource [+ "\0" + assetDigest])`. The orchestrator uses this as the source-tarball cache key and the agent re-computes it against the extracted source to detect drift. |
578
- | `compileSchemaVersion` | Compiler schema version used when computing `contentHash` (currently `5`). The hash input is line-ending-normalized (CRLF → LF) so a lock file produced on Linux matches the agent's hash on Windows where Git's `core.autocrlf=true` rewrites checked-out text to CRLF. Bumping the schema version invalidates every existing source cache entry even if source is unchanged, which is the correct behavior when the compile-time or runtime contract changes. |
579
- | `triggers` | Trigger definitions extracted from the workflow (used by the orchestrator for event matching). |
580
- | `jobs` | Job definitions with scheduling metadata (runsOn, needs, matrix, contexts, concurrency, container, checkout, gracePeriod, label routing, dynamic fields, etc.). |
581
- | `rules` | Workflow-level conditional rules (optional). Stored as dynamic references since rule functions cannot be serialized. |
582
- | `description` | Optional workflow description. |
583
- | `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](https://docs.kici.dev/user/lock-file-and-drift/#extra-files-in-the-content-hash-hashfiles). |
584
- | `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
585
- | `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
586
- | `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](https://docs.kici.dev/user/private-registries/). |
587
- | `installEnv` | Extra qualified secret refs (`<context>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](https://docs.kici.dev/user/private-registries/). |
588
- | `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
589
- | `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
590
- | `approval` | Normalized approval gate (optional): `clauses`, `reason`, `timeoutSeconds`, `when`. When present the whole run is held before any job is dispatched. Job and step entries carry the same normalized block for job- and step-level gates. See [approval gates](https://docs.kici.dev/user/approvals/). |
591
- | `hasFilter` | `true` when the workflow declares a workflow-level `filter` predicate (optional; omitted rather than `false`). The predicate itself is never serialized — the flag tells the orchestrator an agent must evaluate the workflow before any of its jobs is dispatched. See [global workflows](https://docs.kici.dev/user/global-workflows/#narrowing-with-a-filter). |
592
- | Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
574
+ | Field | Description |
575
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
576
+ | `name` | Workflow name. |
577
+ | `source` | Per-workflow source file and export reference. |
578
+ | `contentHash` | SHA-256 of a digest over the whole `.kici/` directory mixed with `compileSchemaVersion` (and an `assetDigest` of declared `hashFiles` when present): `SHA-256(compileSchemaVersion + ":" + treeDigest [+ "\0" + assetDigest])`. The tree digest covers every file under `.kici/` except the paths declared in `.kici/.kiciignore` — see [files the content hash skips](https://docs.kici.dev/user/lock-file-and-drift/#files-the-content-hash-skips-kicikiciignore). Paths are sorted and line endings normalized. The orchestrator uses this as the source-tarball cache key and the agent re-computes it against the extracted tree to detect drift. |
579
+ | `compileSchemaVersion` | Compiler schema version used when computing `contentHash` (currently `7`). The hash input is line-ending-normalized (CRLF → LF) so a lock file produced on Linux matches the agent's hash on Windows where Git's `core.autocrlf=true` rewrites checked-out text to CRLF. Bumping the schema version invalidates every existing source cache entry even if source is unchanged, which is the correct behavior when the compile-time or runtime contract changes. |
580
+ | `triggers` | Trigger definitions extracted from the workflow (used by the orchestrator for event matching). |
581
+ | `jobs` | Job definitions with scheduling metadata (runsOn, needs, matrix, contexts, concurrency, container, checkout, gracePeriod, label routing, dynamic fields, etc.). |
582
+ | `rules` | Workflow-level conditional rules (optional). Stored as dynamic references since rule functions cannot be serialized. |
583
+ | `description` | Optional workflow description. |
584
+ | `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](https://docs.kici.dev/user/lock-file-and-drift/#extra-files-in-the-content-hash-hashfiles). |
585
+ | `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
586
+ | `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
587
+ | `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](https://docs.kici.dev/user/private-registries/). |
588
+ | `installEnv` | Extra qualified secret refs (`<context>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](https://docs.kici.dev/user/private-registries/). |
589
+ | `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
590
+ | `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
591
+ | `approval` | Normalized approval gate (optional): `clauses`, `reason`, `timeoutSeconds`, `when`. When present the whole run is held before any job is dispatched. Job and step entries carry the same normalized block for job- and step-level gates. See [approval gates](https://docs.kici.dev/user/approvals/). |
592
+ | `hasFilter` | `true` when the workflow declares a workflow-level `filter` predicate (optional; omitted rather than `false`). The predicate itself is never serialized — the flag tells the orchestrator an agent must evaluate the workflow before any of its jobs is dispatched. See [global workflows](https://docs.kici.dev/user/global-workflows/#narrowing-with-a-filter). |
593
+ | Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
593
594
 
594
595
  Step entries carry their own capability flags, so the orchestrator can reason about a step without loading your TypeScript:
595
596
 
@@ -670,9 +671,40 @@ kici compile --check
670
671
 
671
672
  This validates all workflows and generates the lock file in memory without writing it. If any workflow has syntax errors or invalid configuration, the command exits non-zero. Pair this with the agent-side hash verification (below) for full drift detection -- `--check` catches broken source, while the agent catches source-lock-file mismatches at run time.
672
673
 
674
+ ## Files the content hash skips (`.kici/.kiciignore`)
675
+
676
+ The per-workflow content hash covers everything under `.kici/` except the paths declared in `.kici/.kiciignore`. `kici init` writes that file for you with this default set:
677
+
678
+ ```
679
+ node_modules/
680
+ types/
681
+ .npmrc
682
+ package-lock.json
683
+ pnpm-lock.yaml
684
+ kici.lock.json
685
+ ```
686
+
687
+ Every entry except `kici.lock.json` names something KiCI itself regenerates. The agent installs your workflow's dependencies before it re-checks the hash, and that install rewrites `package-lock.json`, `pnpm-lock.yaml`, `.npmrc` and `node_modules/`; `kici compile` refreshes `types/` after it has already hashed the tree. Hashing any of them would make the hash change on every run, and the drift gate would reject work that never changed.
688
+
689
+ `kici.lock.json` is different: the hash is written **into** that file, so hashing it would make it an input to itself. It stays excluded whatever your `.kiciignore` says.
690
+
691
+ Patterns are gitignore-style and are matched relative to `.kici/`. A trailing `/` matches a directory and everything beneath it, a bare name matches at any depth, and a pattern containing a slash is anchored at `.kici/`.
692
+
693
+ One rule differs from `git`: **a symlink to a directory counts as a directory**. So `node_modules/` covers a `.kici/node_modules` that is a symlink into a shared dependency tree, where `git` would treat that link as a file. The exclusion means "skip the dependency tree, whatever shape it takes on disk". Hashing the link instead produced a hash your build agent could not reproduce, because its own dependency install always writes a real directory there.
694
+
695
+ A symlink the exclusions do **not** cover is still hashed — as its link target, not as the bytes behind it. The source tarball has to carry that link unchanged for the agent to agree. So `kici compile` warns about a link it cannot carry: one whose target is absolute (extraction strips the leading `/`), or whose target points outside `.kici/`'s parent (extraction drops the link). Point the link inside `.kici/`, replace it with the files it names, or list it in `.kiciignore`.
696
+
697
+ :::caution[The file replaces the defaults — it does not add to them]
698
+ When `.kici/.kiciignore` exists, it **is** the exclusion list. A one-line file excludes one path and re-includes everything else, `package-lock.json` included. `kici compile` warns when your file omits a path a run rewrites, and names both the path and the instability it causes. Delete the file to fall back to the defaults.
699
+ :::
700
+
701
+ `.kiciignore` is itself covered by the hash. Which files define a workflow's identity is part of that identity, so editing the file forces a recompile — and nobody can change what a lock file attests to without changing the lock file.
702
+
703
+ > **Not the repo-root `.kiciignore`.** A `.kiciignore` at the root of your repository is a separate, unrelated file: it selects which working-tree files `kici run remote` uploads. Only the one inside `.kici/` affects the content hash.
704
+
673
705
  ## Extra files in the content hash (`hashFiles`)
674
706
 
675
- By default, the per-workflow content hash is `SHA-256(compileSchemaVersion + ":" + rawSource)` where `rawSource` is the TypeScript text of the workflow entry file. If your workflow depends on files outside `.kici/workflows/` -- configuration files, scripts, Dockerfiles, etc. -- changes to those files will **not** invalidate the cache unless you declare them.
707
+ A helper the workflow imports from `.kici/lib/` is already covered, so editing it invalidates the cache on its own. If your workflow depends on files **outside** `.kici/` -- configuration files, scripts, Dockerfiles, etc. -- changes to those files will **not** invalidate the cache unless you declare them.
676
708
 
677
709
  Use the `hashFiles` option on a workflow to include additional paths or glob patterns (relative to the repo root) in the content hash:
678
710
 
@@ -683,14 +715,14 @@ export default workflow('deploy', {
683
715
  });
684
716
  ```
685
717
 
686
- When any of the matched files change, the content hash formula becomes `SHA-256(compileSchemaVersion + ":" + rawSource + "\0" + assetDigest)` where `assetDigest` is a deterministic encoding of the resolved file paths and their contents. This busts the source-tarball cache and forces the build agent to pack and upload a fresh `source/{contentHash}.tar.gz`. The resolved file paths are recorded in the lock file under `resolvedHashFiles` so the agent can verify without re-discovering the workflow.
718
+ When any of the matched files change, the content hash formula becomes `SHA-256(compileSchemaVersion + ":" + treeDigest + "\0" + assetDigest)` where `assetDigest` is a deterministic encoding of the resolved file paths and their contents. This busts the source-tarball cache and forces the build agent to pack and upload a fresh tarball. The resolved file paths are recorded in the lock file under `resolvedHashFiles` so the agent can verify without re-discovering the workflow.
687
719
 
688
720
  ## Agent-side safety net
689
721
 
690
722
  If drift still occurs (e.g. someone committed only the `.ts` change), the agent detects it at run time before any step runs:
691
723
 
692
- - After extracting the `.kici/` source tarball (or loading source from a `git clone` on the build path), the agent reads the workflow entry file and re-computes `contentHash = SHA-256(compileSchemaVersion + ":" + rawSource [+ "\0" + assetDigest])` using the same formula as the compiler.
693
- - If the orchestrator sent a `contentHash` (from the lock file) and the computed hash does **not** match, the agent fails the run with an error like: **lock file is out of date** (workflow source changed without regenerating the lock file). The error includes the baked agent `@kici-dev/sdk` version + bundle hash so operators can debug cross-host compile mismatches.
724
+ - After extracting the `.kici/` source tarball (or loading source from a `git clone` on the build path), the agent walks the whole extracted `.kici/` tree and re-computes `contentHash = SHA-256(compileSchemaVersion + ":" + treeDigest [+ "\0" + assetDigest])` using the same implementation as the compiler. Because it covers the tree, an edit to any file the workflow imports is caught, not just an edit to the entry file.
725
+ - If the orchestrator sent a `contentHash` (from the lock file) and the computed hash does **not** match, the agent fails the run with an error like: **lock file is out of date** (workflow source changed without regenerating the lock file). The error includes the baked agent `@kici-dev/sdk` version + bundle hash so operators can debug cross-host compile mismatches. When the hashed tree carries symlinks, the error names them too — recompiling cannot reconcile a link the tarball omits or extraction rewrites, so the usual remedy would loop.
694
726
 
695
727
  So even without a pre-commit or CI check, a stale lock file will cause the run to fail with a clear message instead of running with the wrong workflow.
696
728
 
@@ -702,6 +734,7 @@ So even without a pre-commit or CI check, a stale lock file will cause the run t
702
734
  | Catch drift before commit | Install a pre-commit hook with `kici hook install`. |
703
735
  | Catch broken source in CI | Run `kici compile --check` in CI. |
704
736
  | Bust cache on external files | Add `hashFiles: ['config.json']` to include non-workflow files in the content hash. |
737
+ | Skip a path inside `.kici/` | List it in `.kici/.kiciignore` — remember the file replaces the defaults. |
705
738
  | Fail fast when drift remains | Rely on the agent’s hash verification when it compiles from source. |
706
739
 
707
740
  ## See also
@@ -1332,7 +1365,7 @@ Manage the **local dev plane** — the warm, per-user orchestrator (plus its own
1332
1365
  kici local up [--offline | --connected] # Start, or reuse an already-running plane
1333
1366
  kici local status [--json] # Port, pid, PostgreSQL backend, attachment mode, readiness
1334
1367
  kici local down # Stop the orchestrator and its PostgreSQL, verifying the port is freed
1335
- kici local logs # Print the orchestrator log file path
1368
+ kici local logs # Print the plane log paths and rotation policy
1336
1369
  kici local attach # Attach to the Platform (hybrid mode)
1337
1370
  kici local detach # Return the plane to offline (independent) mode
1338
1371
  kici local trust-root <file> # Export the dev-signed trust root for offline verification
@@ -1349,6 +1382,8 @@ The plane runs in one of two modes:
1349
1382
 
1350
1383
  `kici local status` reports a plane whose process is alive but whose readiness probe fails — for example when its PostgreSQL has stopped — as running but not ready, together with its readiness checks, rather than as not running. When the holder is a KiCI plane orchestrator that this config directory did not start — a plane belonging to another `KICI_CONFIG_DIR`, or one whose record here was lost — status names it as such rather than as not ready, since its readiness is never probed, and points at `kici local down`, which does reclaim it. When the port is held by a process that is not a KiCI plane orchestrator, `kici local status` names that holder instead and points at `KICI_LOCAL_ORCH_PORT`, because `kici local down` will not stop it.
1351
1384
 
1385
+ The plane writes its orchestrator log to `orchestrator.log` in its state directory and, when it runs embedded PostgreSQL, the PostgreSQL log to `orchestrator.log.pg` beside it. `kici local logs` prints their location. Each is rotated to a `.1` sibling when it reaches 50 MB, at the next plane start — the plane keeps the current log and one previous generation, so neither can grow without bound.
1386
+
1352
1387
  Pass `--json` for machine-readable output. It prints one object and exits 0 for
1353
1388
  every state, including when the plane is stopped — the state is in the payload,
1354
1389
  not the exit code:
@@ -1641,7 +1676,7 @@ Synopsis: `kici local down`
1641
1676
 
1642
1677
  ### `kici local logs`
1643
1678
 
1644
- Print the local dev plane orchestrator log path
1679
+ Print the local dev plane log paths and rotation policy
1645
1680
 
1646
1681
  Synopsis: `kici local logs`
1647
1682
 
@@ -132,6 +132,8 @@ workflow('test', {
132
132
 
133
133
  When `max: 1` (default), runs are fully serialized within the group.
134
134
 
135
+ `max` is enforced by the orchestrator's database, so the limit is cluster-wide and survives a restart. A run holding a slot keeps it across an orchestrator restart. Every orchestrator in a cluster counts against the same limit, so a group with `max: 1` runs one job at a time no matter which orchestrator dispatched it.
136
+
135
137
  ## Group key examples
136
138
 
137
139
  ### Deploy per environment
@@ -191,6 +193,14 @@ When a workflow has both `concurrency` and `context` protection rules:
191
193
 
192
194
  This means a run that passed approval won't need re-approval if it gets queued by concurrency.
193
195
 
196
+ The two caps also differ in strength. A workflow-level concurrency group claims
197
+ its slot inside a single database transaction, so two runs that arrive together
198
+ cannot both take it. A context
199
+ [concurrency limit](https://docs.kici.dev/user/contexts/#concurrency-limits) is a throughput control:
200
+ simultaneous arrivals can each be admitted before either is recorded. Declare a
201
+ concurrency group for anything that must never run beside itself, whatever the
202
+ context limit says.
203
+
194
204
  ## Cancelling queued runs
195
205
 
196
206
  Queued runs can be cancelled before they start executing. The cancel request removes them from the queue immediately -- they don't go through the grace period since no step is running.
@@ -401,6 +411,17 @@ Store the secrets first with `kici-admin secret set`. Pasting a token straight
401
411
  into the workflow is rejected when the workflow is defined, because a token
402
412
  written into `.kici/` would be committed to your repository.
403
413
 
414
+ The named context's protection rules run before the secret is read, exactly as
415
+ they do for [git credentials](https://docs.kici.dev/user/patterns/git-credentials/#what-a-job-may-ask-for).
416
+ A `prod:` reference from a branch the `prod` context restricts is refused, and
417
+ the job is dispatched with no registry credentials — so a private image fails to
418
+ pull rather than being pulled from a branch the context does not allow. The rule
419
+ that refused it is named in your orchestrator's log, not in the run.
420
+
421
+ **An untrusted ref receives no registry credentials.** A fork pull request is
422
+ dispatched without them, so a private base image fails to pull and a public one
423
+ is unaffected. The run's reduced-privilege note says so.
424
+
404
425
  The username is not a secret, so you may write it directly:
405
426
 
406
427
  ```typescript
@@ -410,6 +431,30 @@ auth: { username: 'ci-bot', tokenSecret: 'prod:REGISTRY_TOKEN' }
410
431
  Your orchestrator resolves these names at dispatch and sends only the resolved
411
432
  credentials to the agent. The agent never reads your secret store.
412
433
 
434
+ ### Naming the registry
435
+
436
+ `auth` also takes a `registry` — the registry host the credentials belong to,
437
+ such as `reg.internal:5000`.
438
+
439
+ With `image` it is optional, because KiCI reads the host off the image
440
+ reference. With `dockerfile` it is **required**: the base image is named inside
441
+ your Dockerfile, so there is nothing to read it from. A `dockerfile` job whose
442
+ `auth` omits `registry` is refused when you define the workflow.
443
+
444
+ ```typescript
445
+ container: {
446
+ dockerfile: '.kici/ci.Dockerfile',
447
+ auth: {
448
+ registry: 'reg.internal:5000',
449
+ usernameSecret: 'prod:REGISTRY_USER',
450
+ tokenSecret: 'prod:REGISTRY_TOKEN',
451
+ },
452
+ },
453
+ ```
454
+
455
+ With `dockerfile`, these credentials pull the Dockerfile's own `FROM` base — not
456
+ a job image, since the job image is the one KiCI builds.
457
+
413
458
  ### Credentials that only exist at run time
414
459
 
415
460
  A token fetched during the run — from a cloud registry's login command, for
@@ -560,10 +605,11 @@ The KiCI CLI reads the following environment variables to customize its behavior
560
605
 
561
606
  ## Development
562
607
 
563
- | Variable | Description | Default |
564
- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
565
- | `KICI_DEV` | Enable development mode. When `true`, uses prerelease-compatible version ranges (`>=0.0.1-0`) for dev dependencies and skips npm version resolution. | unset |
566
- | `KICI_DEBUG` | Enable debug logging. When `true`, prints verbose diagnostics (SDK alias resolution, step-level debug logs, stack traces on errors). Equivalent to the `--debug` CLI flag. | unset |
608
+ | Variable | Description | Default |
609
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
610
+ | `KICI_DEV` | Enable development mode. When `true`, pins `@kici-dev/sdk` to the `latest` dist-tag (so a dev registry's prerelease build resolves) and skips npm version resolution. | unset |
611
+ | `KICI_DEV_REGISTRY` | npm registry the `@kici-dev` scope points at when `KICI_DEV` is set. `kici init` writes it into `.npmrc`; with no value it writes no `.npmrc`. | unset |
612
+ | `KICI_DEBUG` | Enable debug logging. When `true`, prints verbose diagnostics (SDK alias resolution, step-level debug logs, stack traces on errors). Equivalent to the `--debug` CLI flag. | unset |
567
613
 
568
614
  ## Local dev plane
569
615
 
@@ -897,7 +943,7 @@ job('provision', {
897
943
 
898
944
  ### Trust tiers on internal triggers
899
945
 
900
- An internally-triggered run resolves its [trust tier](https://docs.kici.dev/user/contexts/#minimum-trust) from the trigger. The tier decides the run's cache scope, whether it may run a [Dockerfile build](https://docs.kici.dev/user/container-jobs/#who-may-build), whether it receives [install secrets](https://docs.kici.dev/user/private-registries/), and whether a `minimumTrust` context holds it.
946
+ An internally-triggered run resolves its [trust tier](https://docs.kici.dev/user/contexts/#minimum-trust) from the trigger. The tier decides the run's cache scope, whether it may run a [Dockerfile build](https://docs.kici.dev/user/container-jobs/#who-may-build), whether it receives [install secrets](https://docs.kici.dev/user/private-registries/), and whether a `minimumTrust` context holds it. It also decides whether the run's jobs receive [container-registry credentials](https://docs.kici.dev/user/container-jobs/#private-images) and their declared [git credentials](https://docs.kici.dev/user/patterns/git-credentials/#what-a-job-may-ask-for).
901
947
 
902
948
  Four rules resolve the tier, and KiCI applies them in this order:
903
949
 
@@ -1424,7 +1470,7 @@ The registration log line names the organization it decided against, so a refusa
1424
1470
 
1425
1471
  ### Secrets are not available
1426
1472
 
1427
- 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.
1473
+ 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 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.
1428
1474
 
1429
1475
  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.
1430
1476
 
@@ -66,7 +66,7 @@ You declare a gate in your workflow with `approval`. It is available at three le
66
66
  - **Job** — hold the job before any of its steps run.
67
67
  - **Workflow** — hold the whole run before any job is dispatched.
68
68
 
69
- A step-level gate can also fire **only when a check/apply step finds drift** — Terraform's plan→apply, per step. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-whendrift) below.
69
+ A step-level gate can also fire **only when a check/apply step finds drift** — Terraform's plan→apply, per step. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-when-drift) below.
70
70
 
71
71
  Approvers are named as **teams** and **users**. A team is an operator-defined group of org members; your workflow code may name a team but can never change its membership, which is what makes a team clause a real gate rather than a suggestion. See [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) for how operators define teams, the approval queue, and expiry; see [the architecture overview](https://docs.kici.dev/architecture/approvals/) for how a hold is evaluated and resumed.
72
72
 
@@ -137,7 +137,7 @@ approval: {
137
137
 
138
138
  | Field | Type | Description |
139
139
  | ----------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
- | `when` | `'always' \| 'drift'` | When the gate fires. `'always'` (default) gates before the element; `'drift'` gates a check/apply step only when it finds drift. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-whendrift). |
140
+ | `when` | `'always' \| 'drift'` | When the gate fires. `'always'` (default) gates before the element; `'drift'` gates a check/apply step only when it finds drift. See [Drift gates](https://docs.kici.dev/user/approvals/#drift-gates-when-drift). |
141
141
  | `approvers` | `ApproverClause[]` | The AND list of `{ team }` / `{ user }` clauses. An empty list means "any approval-capable member". |
142
142
  | `reason` | `string` | A human-readable label shown in the dashboard queue and the held-for-approval status check. |
143
143
  | `timeout` | `number` | Per-gate expiry in **seconds**, overriding the org default. Must be a **positive integer** number of seconds; a non-positive or non-finite value is rejected at compile time. On expiry the element is rejected. |
@@ -316,7 +316,7 @@ Available rules:
316
316
  - **Branch restrictions** — only allow specific branches to deploy.
317
317
  - **Required reviewer approvals** — gate the run on human sign-off.
318
318
  - **Wait timers** — delay execution for a fixed period.
319
- - **Concurrency limits** — prevent collisions between parallel deployments.
319
+ - **Concurrency limits** — cap how many jobs run against the context at once.
320
320
 
321
321
  <!-- /help:contexts-protection -->
322
322
 
@@ -405,7 +405,7 @@ job('deploy', {
405
405
 
406
406
  **Skip-on-test (allow-and-warn).** On a test or local run (`kici run remote`, `kici run <event> --local`), a bound context never rejects the run. Any bound context that disallows local execution (`allowLocalExecution: false`) — or that is not configured — is **skipped**: its variables and secrets are omitted from the merge and its gates are not evaluated. The run proceeds, and a user-visible warning naming the skipped context(s) is shown both on the `kici run remote` CLI output and on the dashboard run view. This makes the test-only-variables pattern work: with `contexts: ['staging', 'my-testing']` where only `my-testing` allows local execution, a test run resolves just `my-testing`'s variables and warns that `staging` was skipped. If every bound context is skipped, the job runs with no environment variables. This is intentionally different from a fixture `secrets:` mapping, which is fail-closed — see the [testing guide](https://docs.kici.dev/user/testing-guide/).
407
407
 
408
- **Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context) simply adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic context resolving to an as-yet-unconfigured name does today.
408
+ **Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context) adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic context resolving to an as-yet-unconfigured name does today.
409
409
 
410
410
  **Registration rejects a provably-unsatisfiable binding.** When a workflow is registered, KiCI statically checks every multi-context binding: a bound context that does not exist, a disabled one, or two contexts with mutually-exclusive fixed branch / trigger-type / repository restrictions (no value can satisfy both) makes the binding provably unsatisfiable, and the registration is rejected with a precise message naming the job, the contexts, and the rule — for example `unsatisfiable context binding: job 'deploy' binds contexts [staging, my-testing] with mutually exclusive branch restrictions (no value satisfies all bound contexts)`. Bindings whose restrictions use globs are undecidable at registration and fall through to the dispatch-time gate check instead.
411
411
 
@@ -608,6 +608,36 @@ The children of a matrix job count individually against the limit. A three-child
608
608
  matrix bound to a context with a limit of two dispatches two children and applies
609
609
  the strategy above to the third.
610
610
 
611
+ A job counts against the limit from the moment it is dispatched until it
612
+ finishes. It does not have to reach an agent first.
613
+
614
+ #### What the limit does and does not guarantee
615
+
616
+ A context concurrency limit is a **throughput control**. Treat it as a cap on
617
+ how much work runs at once, not as a lock.
618
+
619
+ Two events that arrive in the same instant read the limit before either job is
620
+ recorded, so each can be admitted. The window is short — the time between one
621
+ read and one write — but it is real, and it grows with the number of
622
+ orchestrator processes serving the context.
623
+
624
+ When a job must never run beside another copy of itself — a production deploy,
625
+ a database migration — declare a workflow-level
626
+ [concurrency group](https://docs.kici.dev/user/concurrency/) as well:
627
+
628
+ ```typescript
629
+ export default workflow('deploy', {
630
+ on: push({ branches: ['main'] }),
631
+ concurrency: { group: () => 'deploy-prod', max: 1 },
632
+ jobs: [/* ... */],
633
+ });
634
+ ```
635
+
636
+ That mechanism claims its slot inside a single database transaction, so two
637
+ runs arriving together cannot both take it. The two are complementary: the
638
+ context limit caps throughput across every workflow bound to the context, and
639
+ the concurrency group serializes one workflow against itself.
640
+
611
641
  ## Dashboard management
612
642
 
613
643
  ### Creating contexts
@@ -914,7 +944,7 @@ registries: [
914
944
  ## Security model
915
945
 
916
946
  - **Per-context scoping.** Every `tokenSecret` and `installEnv` entry is qualified with a context name. The orchestrator runs the same protection-rule pipeline (branch / trust / concurrency / reviewer / wait-timer) against each named context **before** resolving any secret, so a workflow that wants a `production` token from a feature branch is rejected exactly like a job that tries to deploy to `production` from a feature branch. A reviewer-gated install context **pauses** the whole workflow dispatch as a workflow-scoped held run instead of resolving the token — see [Reviewer-gated installs](https://docs.kici.dev/user/private-registries/#reviewer-gated-installs) below.
917
- - **Untrusted refs get no tokens.** When the trust resolution returns anything other than `trusted` — every fork pull request does — the orchestrator strips both `npmRegistries` and `installEnvSecrets` out of the dispatch. The install runs without auth and fails naturally on the first private dep. A fork pull request cannot observe a registry token, even if a context lacks an explicit [minimum trust](https://docs.kici.dev/user/contexts/#minimum-trust) rule.
947
+ - **Untrusted refs get no tokens.** When the trust resolution returns anything other than `trusted` — every fork pull request does — the orchestrator strips `npmRegistries`, `installEnvSecrets`, and a container job's [registry credentials](https://docs.kici.dev/user/container-jobs/#private-images) out of the dispatch. The install runs without auth and fails naturally on the first private dep, and a private base image fails to pull. A fork pull request cannot observe a registry token, even if a context lacks an explicit [minimum trust](https://docs.kici.dev/user/contexts/#minimum-trust) rule.
918
948
  - **Lifecycle scripts disabled.** Whenever a private registry is in scope, the agent runs the install with `--ignore-scripts` (npm, pnpm, and yarn classic alike; yarn berry gets the equivalent `enableScripts: false`). A malicious `preinstall` / `postinstall` hook in committed `package.json` cannot read the synthesized token env vars, even though they exist in the install subprocess. For a pnpm or yarn workspace, the agent builds your in-repo dependency closure as a separate step **after** the install's auth is torn down, so build scripts never see the tokens either.
919
949
  - **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
920
950
  - **Job-scoped env-var names.** The synthesized auth env var is `KICI_NPM_TOKEN_<jobIdShort>_<i>` where `jobIdShort` is the first 8 chars of the dispatched job id. The name is unguessable from outside the install subprocess and not reused across jobs.
@@ -950,7 +980,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
950
980
  ## See also
951
981
 
952
982
  - [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<context>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
953
- - [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
983
+ - [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (branch restrictions, required reviewers, minimum trust) that the install gate inherits.
954
984
  - [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.
955
985
 
956
986
  ---
@@ -1068,6 +1098,107 @@ and the step never holds signing credentials — the orchestrator mints and sign
1068
1098
  the token on the step's behalf from its own run records. Like `attestProvenance`,
1069
1099
  it is only available inside a running job step.
1070
1100
 
1101
+ ## ID-token claims and cloud trust policies
1102
+
1103
+ A cloud provider's OIDC trust policy decides which builds may assume a role. The
1104
+ token below is what your policy matches on, so read this section before you
1105
+ write one.
1106
+
1107
+ ### The claim set
1108
+
1109
+ | Claim | Value |
1110
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- |
1111
+ | `iss` | Your orchestrator's provenance issuer |
1112
+ | `aud` | The audience you asked for |
1113
+ | `sub` | The build identity — see the two shapes below |
1114
+ | `repository` | `owner/repo` the run acted on |
1115
+ | `ref` | The branch or tag the run PRESENTS. For a pull request this is the **base** branch, not the contributor's branch |
1116
+ | `base_ref` | The same value as `ref`, named the way GitHub Actions names it |
1117
+ | `head_ref` | The pull request's HEAD branch; `''` for a non-PR run |
1118
+ | `head_repository` | `owner/repo` of the pull-request HEAD — the contributor's fork for a fork PR; `''` for a non-PR run |
1119
+ | `is_fork` | `'true'`, `'false'`, or `'unresolved'` |
1120
+ | `event_name` | The event that started the run (`push`, `pull_request:opened`, `schedule`, …) |
1121
+ | `trust_tier` | The resolved trust tier of the triggering actor, or `'unresolved'` |
1122
+ | `actor` | Provider login of the triggering actor |
1123
+ | `sha` | The run's commit |
1124
+ | `workflow_ref` | `<workflow name>@<sha>` |
1125
+ | `kici_run_id` / `kici_job_id` | The run and job this token was minted for |
1126
+ | `org_id` | Your organization id |
1127
+
1128
+ Every claim in the table is **always present**. A value the run did not resolve
1129
+ is `''` or `'unresolved'`, never omitted and never guessed. That matters: an
1130
+ absent claim makes a `StringEquals` condition pass, which would silently remove
1131
+ a constraint you wrote expecting it to be enforced.
1132
+
1133
+ ### The two `sub` shapes
1134
+
1135
+ ```
1136
+ push, tag, schedule, … repo:<owner/repo>:ref:<ref>:workflow:<workflow name>
1137
+ pull request, review repo:<owner/repo>:pull_request
1138
+ ```
1139
+
1140
+ The pull-request shape carries **no ref segment**, mirroring GitHub Actions. A
1141
+ pull request's `ref` is its base branch. So a ref-bearing subject would be
1142
+ identical for a fork pull request targeting `main` and a trusted push to `main`.
1143
+ A policy pinning that subject would hand your cloud role to any contributor who
1144
+ opened a pull request running the same workflow.
1145
+
1146
+ **A re-run keeps the shape of the run it repeats.** Re-running a pull-request
1147
+ run presents `repo:<owner/repo>:pull_request`, because it rebuilds the same
1148
+ commit from the same source. Its `event_name` claim still reads `rerun` — that
1149
+ claim says what started the run, while `sub` says which identity the run
1150
+ presents. A policy that pins the branch-shaped subject therefore does not match
1151
+ a re-run of a pull request, which is the same protection the first run gets.
1152
+
1153
+ ### A worked AWS trust policy
1154
+
1155
+ Pin `sub`, and pin the fork context too. `sub` alone tells you a pull request
1156
+ ran; it does not tell you whose code ran.
1157
+
1158
+ ```json
1159
+ {
1160
+ "Version": "2012-10-17",
1161
+ "Statement": [
1162
+ {
1163
+ "Effect": "Allow",
1164
+ "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/orch.example.com" },
1165
+ "Action": "sts:AssumeRoleWithWebIdentity",
1166
+ "Condition": {
1167
+ "StringEquals": {
1168
+ "orch.example.com:aud": "sts.amazonaws.com",
1169
+ "orch.example.com:sub": "repo:acme/app:ref:main:workflow:deploy",
1170
+ "orch.example.com:is_fork": "false",
1171
+ "orch.example.com:head_repository": "acme/app",
1172
+ "orch.example.com:trust_tier": "trusted"
1173
+ }
1174
+ }
1175
+ }
1176
+ ]
1177
+ }
1178
+ ```
1179
+
1180
+ This grants the role only to a run on `main` in `acme/app`, from code in that
1181
+ same repository, triggered by an actor your orchestrator resolved as trusted.
1182
+ A fork pull request fails on all three of the extra conditions, and a run whose
1183
+ context did not resolve fails too — `'unresolved'` matches none of them, so the
1184
+ policy fails closed.
1185
+
1186
+ To let a same-repo pull request assume the role, add a second statement pinning
1187
+ `"sub": "repo:acme/app:pull_request"` alongside `"is_fork": "false"` and
1188
+ `"head_repository": "acme/app"`.
1189
+
1190
+ ### Migrating an existing policy
1191
+
1192
+ If you already pin a ref-bearing `sub` for pull-request runs, that policy stops
1193
+ matching once you upgrade — which is the fix, because it was matching runs it
1194
+ should not have. Move it to `repo:<owner/repo>:pull_request` plus the fork
1195
+ conditions above. The same move covers a re-run of a pull request, which
1196
+ presents the pull-request subject too.
1197
+
1198
+ While you migrate, `KICI_OIDC_LEGACY_PR_SUB=1` on the orchestrator restores the
1199
+ old subject. It restores the collision with it, so treat it as a short bridge,
1200
+ not a setting. See [deprecations](https://docs.kici.dev/user/deprecations/).
1201
+
1071
1202
  ## Verifying an attestation
1072
1203
 
1073
1204
  Verify a bundle with the `kici verify-attestation` command. It establishes the