@kici-dev/compiler 0.7.0 → 0.9.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 (55) hide show
  1. package/dist/cli.js +5 -4
  2. package/dist/commands/docs.js +2 -2
  3. package/dist/commands/feedback.d.ts +23 -1
  4. package/dist/commands/feedback.js +86 -9
  5. package/dist/commands/index.d.ts +2 -2
  6. package/dist/commands/index.js +2 -2
  7. package/dist/commands/local.d.ts +13 -5
  8. package/dist/commands/local.js +19 -8
  9. package/dist/commands/report/identity.js +1 -1
  10. package/dist/commands/run-banner.d.ts +1 -1
  11. package/dist/commands/run-banner.js +1 -1
  12. package/dist/commands/run-routed.js +3 -0
  13. package/dist/commands/run.js +5 -2
  14. package/dist/commands/runs/logs.js +3 -2
  15. package/dist/commands/verify-attestation.js +1 -1
  16. package/dist/llm-context/llms-architecture.txt +9 -7
  17. package/dist/llm-context/llms-cli-remote.txt +27 -12
  18. package/dist/llm-context/llms-cli.txt +6 -4
  19. package/dist/llm-context/llms-features-execution.txt +9 -9
  20. package/dist/llm-context/llms-features.txt +452 -133
  21. package/dist/llm-context/llms-full.txt +561 -197
  22. package/dist/llm-context/llms-getting-started.txt +34 -17
  23. package/dist/llm-context/llms-providers.txt +2 -2
  24. package/dist/llm-context/llms-sdk-runtime.txt +16 -2
  25. package/dist/llm-context/llms-sdk.txt +7 -12
  26. package/dist/llm-context/llms.txt +7 -6
  27. package/dist/local-plane/orchestrator-process.d.ts +4 -5
  28. package/dist/local-plane/orchestrator-process.js +5 -1
  29. package/dist/local-plane/paths.d.ts +1 -0
  30. package/dist/local-plane/paths.js +1 -0
  31. package/dist/local-plane/plane-log.d.ts +27 -0
  32. package/dist/local-plane/plane-log.js +39 -0
  33. package/dist/local-plane/plane-manager.js +2 -2
  34. package/dist/local-plane/plane-trigger.d.ts +28 -0
  35. package/dist/local-plane/plane-trigger.js +57 -2
  36. package/dist/local-plane/postgres.js +9 -6
  37. package/dist/local-plane/run-follow.js +2 -1
  38. package/dist/remote/output/streaming.d.ts +12 -0
  39. package/dist/remote/output/streaming.js +20 -1
  40. package/dist/remote/platform-client.d.ts +2 -0
  41. package/dist/templates/agents-md.d.ts +1 -1
  42. package/dist/templates/agents-md.js +9 -7
  43. package/dist/templates/package-json.d.ts +9 -7
  44. package/dist/templates/package-json.js +11 -9
  45. package/dist/templates/workflows/hello-world.ts +1 -1
  46. package/dist/templates/workflows/pr-checks.ts +2 -2
  47. package/dist/test-runner/job-executor.js +3 -2
  48. package/dist/types.d.ts +12 -35
  49. package/dist/types.js +2 -12
  50. package/dist/types.test-d.d.ts +2 -0
  51. package/dist/types.test-d.js +63 -0
  52. package/dist/workflows/hello-world.ts +1 -1
  53. package/dist/workflows/pr-checks.ts +2 -2
  54. package/package.json +15 -15
  55. package/sbom.spdx.json +1303 -1483
@@ -588,7 +588,7 @@ Point to a local SDK checkout for IDE autocompletion:
588
588
  {
589
589
  "name": "my-project-kici",
590
590
  "devDependencies": {
591
- "@kici-dev/sdk": ">=0.0.1-0"
591
+ "@kici-dev/sdk": "latest"
592
592
  },
593
593
  "kici": {
594
594
  "sdkPath": "../../packages/sdk"
@@ -600,7 +600,7 @@ The `sdkPath` field tells the compiler where to resolve TypeScript path mappings
600
600
 
601
601
  ### KICI_DEV environment variable
602
602
 
603
- Set `KICI_DEV=true` to use a prerelease-compatible version range (`>=0.0.1-0`) in generated files, which resolves prerelease builds from a local Verdaccio registry:
603
+ Set `KICI_DEV=true` to pin the SDK to the `latest` dist-tag in generated files, which resolves prerelease builds from a local Verdaccio registry:
604
604
 
605
605
  ```bash
606
606
  KICI_DEV=true npx kici init
@@ -845,7 +845,7 @@ Each guide is split into two parts. **Part 1** gets you a green run against your
845
845
 
846
846
  ## Bring a coding agent
847
847
 
848
- Workflows are TypeScript, so a coding agent can write them. KiCI ships its docs in a form an agent reads directly: point yours at [llms.txt](https://kici.dev/docs/llms.txt), or pipe a task bundle straight into its context with `kici docs llm` (`kici docs llm sdk` for the authoring API, `kici docs llm patterns` for recipes). An agent briefed that way can scaffold your first workflow, compile it, and read the failure when it breaks.
848
+ Workflows are TypeScript, so a coding agent can write them. KiCI ships its docs in a form an agent reads directly: point yours at [llms.txt](https://kici.dev/llms.txt), or pipe a task bundle straight into its context with `kici docs llm` (`kici docs llm sdk` for the authoring API, `kici docs llm patterns` for recipes). An agent briefed that way can scaffold your first workflow, compile it, and read the failure when it breaks.
849
849
 
850
850
  If your agent finds that these docs promise something KiCI does not do, that is worth telling us — `kici feedback` prints how to report it, and [Reporting a discrepancy](https://docs.kici.dev/user/reporting-discrepancies/) is the full guide.
851
851
 
@@ -891,8 +891,10 @@ promised. You can report that.
891
891
  A human reader can use this page too. The rules below exist because the tracker
892
892
  is public and you are filing under someone else's GitHub identity.
893
893
 
894
- Run `kici feedback` to print this contract in your terminal, or
895
- `kici feedback --json` to read it as structured data.
894
+ Run `kici feedback` to print this contract in your terminal,
895
+ `kici feedback --json` to read it as structured data, or
896
+ `kici feedback --draft <file.json> --open` to turn a draft into the prefilled form
897
+ and open it.
896
898
 
897
899
  ## The rule, in one sentence
898
900
 
@@ -967,19 +969,20 @@ approval rule.
967
969
 
968
970
  ## What the report must carry
969
971
 
970
- Five things. A report missing any of them cannot be acted on:
972
+ A report missing any of these fields cannot be acted on:
971
973
 
972
974
  | Field | What it holds |
973
975
  | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
974
976
  | What the docs or CLI advertise | The exact claim, quoted, plus its source: a docs URL or the command whose `--help` says it. |
975
977
  | What actually happened | The real output or behaviour, quoted, with any error text. |
976
978
  | Minimal reproduction, including setup | Every step from an empty directory: the setup commands, a minimal synthetic workflow, and the exact command you ran. |
977
- | Version and environment | Output of `kici --version`, plus Node version and OS. |
979
+ | KiCI version | Output of `kici --version`. |
980
+ | Environment | Node version and OS. |
978
981
  | Why this is a discrepancy | One or two sentences ruling out the likely misreads — why the docs cannot be read to match what you observed. |
979
982
 
980
983
  The last field is the one agents skip, and it is the one that makes a report
981
984
  usable. "The docs say `--foo` exists and it does not" is a claim. "`--foo` is
982
- documented at <url>, and `kici bar --help` on 0.1.16 lists no such flag" is a
985
+ documented at <url>, and `kici bar --help` on the version I ran lists no such flag" is a
983
986
  finding someone can act on in one pass.
984
987
 
985
988
  Include setup steps whenever your reproduction needed any. A reproduction that
@@ -988,22 +991,36 @@ starts from state a maintainer cannot recreate is not a reproduction.
988
991
  ## Filing it
989
992
 
990
993
  The tracker is [kici-dev/kici-public](https://github.com/kici-dev/kici-public),
991
- and the form is **Agent report**:
994
+ and the form is **Agent report** (`agent_report.yml`). It is a GitHub issue
995
+ form, which only the browser can submit — `gh issue create` cannot fill it.
996
+ So you draft, and the person files.
992
997
 
993
- ```bash
994
- kici feedback --open
998
+ Write the draft as JSON, one key per field of the form (`kici feedback --json`
999
+ lists them under `draftFields`):
1000
+
1001
+ ```json
1002
+ {
1003
+ "title": "kici compile --check writes a lock file",
1004
+ "advertised": "https://docs.kici.dev/user/cli-reference/ says: `kici compile --check` type-checks without writing a lock file.",
1005
+ "observed": ".kici/kici.lock.json was written; mtime changed.",
1006
+ "reproduction": "$ mkdir /tmp/repro && cd /tmp/repro\n$ kici init\n$ kici compile --check",
1007
+ "version": "0.8.0",
1008
+ "environment": "Node 24.13.0, Ubuntu 24.04",
1009
+ "justification": "The sentence names --check as the no-write mode; no reading of it allows a write."
1010
+ }
995
1011
  ```
996
1012
 
997
- Draft the complete body first. Show it to the person you are working with —
998
- the whole body, not a summary of it. File only after they say yes:
1013
+ Then build the prefilled form:
999
1014
 
1000
1015
  ```bash
1001
- gh issue create --repo kici-dev/kici-public --template agent_report.yml
1016
+ kici feedback --draft draft.json --open
1002
1017
  ```
1003
1018
 
1004
- The form asks you to confirm the report was drafted by an agent and reviewed by
1005
- a person before filing. Answer honestly; it is what lets maintainers triage
1006
- agent-filed reports as a group.
1019
+ The command prints the form URL with every field filled in and, with
1020
+ `--open`, opens it. It reaches no network and files nothing. Show the person
1021
+ you are working with the whole draft — not a summary of it. They open the
1022
+ URL, read the prefilled form, tick the review boxes, and submit. File only
1023
+ after they say yes.
1007
1024
 
1008
1025
  If they say no, that is the end of it. Do not re-file the same finding in
1009
1026
  another form, and do not open it as a question instead.
@@ -758,8 +758,8 @@ kici-admin org-settings global-workflows deny-add \
758
758
  ```
759
759
 
760
760
  See [Global workflows](https://docs.kici.dev/architecture/global-workflows/) for the
761
- policy model (`isWorkflowRepoAllowed` + `isSourceRepoAllowed` +
762
- `isElevatedAccessAllowed`) and the cross-provider dispatch contract.
761
+ policy model (`isWorkflowRepoAllowed` + `isSourceRepoAllowed`) and the
762
+ cross-provider dispatch contract.
763
763
 
764
764
  ## Routing-key collisions
765
765
 
@@ -1332,6 +1332,17 @@ interface StepContext<TInputs = Record<string, unknown>> {
1332
1332
  provider?: string;
1333
1333
  /** Whether this execution was triggered by `kici run remote` (developer-initiated remote run) */
1334
1334
  isTestRun: boolean;
1335
+ /** The job's own checked-out repository (present for every job that checks out); `withWrite` opens a write window for it — see [Git credentials](https://docs.kici.dev/user/patterns/git-credentials/) */
1336
+ repo?: {
1337
+ identifier: string;
1338
+ path: string;
1339
+ ref?: string;
1340
+ sha?: string;
1341
+ withWrite(
1342
+ opts: { permissions?: Record<string, string>; credential?: string },
1343
+ fn: () => Promise<void>,
1344
+ ): Promise<void>;
1345
+ };
1335
1346
  /** Registering repo of a global workflow (undefined for non-global workflows) */
1336
1347
  workflowRepo?: RepoInfo;
1337
1348
  /** Repo where the triggering event occurred (undefined for non-global workflows) */
@@ -1532,13 +1543,15 @@ const deploy = job('deploy', {
1532
1543
 
1533
1544
  Request a short-lived OIDC ID token for the current job, bound to an `audience`. The token is a signed JWT whose identity claims (`repository`, `ref`, `sha`, `kici_run_id`, `kici_job_id`) are derived by your orchestrator from the run context — a step cannot spoof them. Use it to authenticate the build to an external service that trusts the orchestrator's OIDC issuer (for example, when generating build provenance).
1534
1545
 
1535
- The token also carries the event context a cloud trust policy needs to tell a fork pull request from a trusted push — `is_fork`, `head_repository`, `trust_tier`, `event_name`, and a pull-request-specific `sub`. See [ID-token claims and cloud trust policies](https://docs.kici.dev/user/provenance/#id-token-claims-and-cloud-trust-policies) for the full claim table and a worked AWS policy.
1546
+ The token also carries the event context a cloud trust policy needs to tell a fork pull request from a trusted push — `is_fork`, `head_repository`, `trust_tier`, `event_name`, and a pull-request-specific `sub`. See [ID-token claims and cloud trust policies](https://docs.kici.dev/user/oidc/#id-token-claims-and-cloud-trust-policies) for the full claim table and a worked AWS policy.
1536
1547
 
1537
1548
  ```typescript
1538
1549
  const publish = job('publish', {
1539
1550
  steps: [
1540
1551
  step('mint', async (ctx) => {
1541
- const { token, expiresIn } = await ctx.kici.oidc.token({ audience: 'sigstore' });
1552
+ const minted = await ctx.kici.oidc.token({ audience: 'sigstore' });
1553
+ if ('deferred' in minted) throw new Error(`ID token deferred: ${minted.code}`);
1554
+ const { token, expiresIn } = minted;
1542
1555
  ctx.log.info(`Got an ID token valid for ${expiresIn}s`);
1543
1556
  // Hand `token` to a tool that exchanges it with the trusting service.
1544
1557
  }),
@@ -1548,6 +1561,7 @@ const publish = job('publish', {
1548
1561
 
1549
1562
  **Behavior:**
1550
1563
 
1564
+ - The result is the minted token, or `{ deferred: true, code }` (`unavailable` | `failed`) when the orchestrator could not mint one right now. A step that exchanges the token fails on `deferred`; `ctx.attestProvenance` handles it by freezing the statement for later fulfilment.
1551
1565
  - The token is short-lived (about 10 minutes) and scoped to the current run and job.
1552
1566
  - The returned token value is automatically masked in step logs.
1553
1567
  - The step never holds signing credentials — the orchestrator mints and signs the token on the step's behalf from its own run records.
@@ -1051,9 +1051,8 @@ The throw is a `ChangedFilesUnavailableError` (exported from `@kici-dev/sdk`, ca
1051
1051
  ### evaluateRules(rules, context, label, onRuleResult?)
1052
1052
 
1053
1053
  The agent calls this on your behalf. A workflow does not call it. It lives on
1054
- `@kici-dev/sdk/internal`, outside semver, and stays exported from the root barrel as
1055
- `@deprecated` until v1.0.0 see [deprecations](https://docs.kici.dev/user/deprecations/). It is described here
1056
- because its return shape is what a rule's outcome looks like in the run log.
1054
+ `@kici-dev/sdk/internal`, outside semver, and is not exported from the root barrel. It is
1055
+ described here because its return shape is what a rule's outcome looks like in the run log.
1057
1056
 
1058
1057
  Evaluate an array of rules sequentially with fail-fast behavior. Stops on the first failure.
1059
1058
 
@@ -1306,9 +1305,8 @@ isDynamicFunction(matrix); // true if async function
1306
1305
  ### Matrix expansion utilities
1307
1306
 
1308
1307
  The agent expands a matrix for you. A workflow does not call these. They live on
1309
- `@kici-dev/sdk/internal`, outside semver, and stay exported from the root barrel as
1310
- `@deprecated` until v1.0.0 see [deprecations](https://docs.kici.dev/user/deprecations/). They are described
1311
- here because they define the combinations a matrix job actually produces.
1308
+ `@kici-dev/sdk/internal`, outside semver, and are not exported from the root barrel. They are
1309
+ described here because they define the combinations a matrix job actually produces.
1312
1310
 
1313
1311
  ```typescript
1314
1312
  import { expandMatrix, applyIncludeExclude } from '@kici-dev/sdk/internal';
@@ -1905,7 +1903,7 @@ Create a commit status trigger. Returns a frozen `StatusTriggerConfig`.
1905
1903
  function status(config?: StatusConfigInput): StatusTriggerConfig;
1906
1904
  ```
1907
1905
 
1908
- **Config options:** `contexts` (picomatch strings like 'ci/\*'), `states` (error/failure/pending/success), `description`
1906
+ **Config options:** `contexts` (glob patterns like 'ci/\*'), `states` (error/failure/pending/success), `description`
1909
1907
 
1910
1908
  ```typescript
1911
1909
  status(); // Any status
@@ -2531,11 +2529,8 @@ a `.result` proxy reads, build the step context your workflow body receives, eva
2531
2529
  rules, and expand its matrix. The agent drives all of it on your behalf.
2532
2530
 
2533
2531
  It is **not covered by semver** and may change shape in any release. Do not import it from
2534
- a workflow. Everything a workflow author needs is on the root entry point above.
2535
-
2536
- Those same symbols are also still exported from the root barrel, marked `@deprecated`, so
2537
- an older SDK in a repository keeps working. They are removed from the root at v1.0.0 — see
2538
- [deprecations](https://docs.kici.dev/user/deprecations/).
2532
+ a workflow. Everything a workflow author needs is on the root entry point above, and none
2533
+ of the runtime contract is exported there.
2539
2534
 
2540
2535
  ## See also
2541
2536
 
@@ -1,6 +1,6 @@
1
1
  # KiCI
2
2
 
3
- > KiCI is a TypeScript-native CI/CD workflow engine. Workflows are defined in TypeScript (not YAML), compiled into a portable lock file, and executed by self-hosted agents. The docs below cover the SDK, the CLI, workflow patterns, and the runtime architecture an LLM coding agent needs in order to author and test KiCI workflows.
3
+ > KiCI is a TypeScript-native CI/CD workflow engine. Workflows are defined in TypeScript (not YAML), compiled into a portable lock file, and executed by self-hosted agents. The docs below cover the SDK, the CLI, workflow patterns, and the runtime architecture an LLM coding agent needs to author and test KiCI workflows.
4
4
 
5
5
  The full markdown bundle of every page indexed here is available at https://docs.kici.dev/llms-full.txt.
6
6
 
@@ -10,16 +10,16 @@ Found a mismatch between what these docs advertise and what KiCI does? Read http
10
10
 
11
11
  Each bundle below is a self-contained markdown file for one authoring task. Fetch only the one your task needs instead of the full bundle:
12
12
 
13
- - [getting-started](https://docs.kici.dev/llms-getting-started.txt) (65 KB) — Adopt KiCI: why it exists, how workflows execute, migrating from GitHub Actions, installing the SDK, and writing/compiling/testing your first workflow
13
+ - [getting-started](https://docs.kici.dev/llms-getting-started.txt) (66 KB) — Adopt KiCI: why it exists, how workflows execute, migrating from GitHub Actions, installing the SDK, and writing/compiling/testing your first workflow
14
14
  - [patterns](https://docs.kici.dev/llms-patterns.txt) (61 KB) — Copy-paste workflow recipes: triggers, conditionals, matrix, scheduling, integrations
15
15
  - [sdk](https://docs.kici.dev/llms-sdk.txt) (131 KB) — Core authoring API: workflow/job/step factories, triggers, rules, matrix, dynamic jobs, cross-job outputs
16
- - [sdk-runtime](https://docs.kici.dev/llms-sdk-runtime.txt) (115 KB) — Runtime and advanced authoring: step runtime context, event payloads, host fan-out, idempotent steps, caching, artifacts, waiting
16
+ - [sdk-runtime](https://docs.kici.dev/llms-sdk-runtime.txt) (116 KB) — Runtime and advanced authoring: step runtime context, event payloads, host fan-out, idempotent steps, caching, artifacts, waiting
17
17
  - [cli](https://docs.kici.dev/llms-cli.txt) (97 KB) — Running the CLI locally: compile, test, run local, hooks, lock-file drift, common failures
18
- - [cli-remote](https://docs.kici.dev/llms-cli-remote.txt) (109 KB) — Auth, org and orchestrator selection, runs, approvals, notifications, diagnostics, and the MCP server a coding agent connects to
19
- - [features](https://docs.kici.dev/llms-features.txt) (97 KB) — Deployment contexts, scoped secrets, private registries, approvals, provenance, dashboard and account access
18
+ - [cli-remote](https://docs.kici.dev/llms-cli-remote.txt) (111 KB) — Auth, org and orchestrator selection, runs, approvals, notifications, diagnostics, and the MCP server a coding agent connects to
19
+ - [features](https://docs.kici.dev/llms-features.txt) (112 KB) — Deployment contexts, scoped secrets, private registries, approvals, workload identity, provenance, dashboard and account access
20
20
  - [features-execution](https://docs.kici.dev/llms-features-execution.txt) (124 KB) — Concurrency, dynamic values, events, container jobs, environment variables, global workflows, idempotent steps
21
21
  - [providers](https://docs.kici.dev/llms-providers.txt) (35 KB) — Connecting sources: GitHub App, universal-git (Forgejo/Gitea/GitLab), local file://
22
- - [architecture](https://docs.kici.dev/llms-architecture.txt) (100 KB) — How the runtime works: three-tier relay model, data flows, configuration
22
+ - [architecture](https://docs.kici.dev/llms-architecture.txt) (101 KB) — How the runtime works: three-tier relay model, data flows, configuration
23
23
 
24
24
  ## Getting started
25
25
 
@@ -86,6 +86,7 @@ Each bundle below is a self-contained markdown file for one authoring task. Fetc
86
86
  - [Approval gates](https://docs.kici.dev/user/approvals/): Pause a workflow for human sign-off at step, job, or workflow granularity with approval
87
87
  - [Contexts](https://docs.kici.dev/user/contexts/): Configure deployment contexts with variables, secrets, and protection rules
88
88
  - [Dashboard](https://docs.kici.dev/user/dashboard/): Web UI for monitoring workflow runs, managing sources, secrets, and organization settings.
89
+ - [Workload identity with OIDC](https://docs.kici.dev/user/oidc/): Mint short-lived OIDC identity tokens in a step and exchange them with Cloudsmith, AWS, or any service that federates with your orchestrator
89
90
  - [Private npm registries](https://docs.kici.dev/user/private-registries/): Authenticate `npm install` against private registries (CodeArtifact, GitHub Packages, Verdaccio, …) from a workflow's `.kici/package.json`
90
91
  - [Build provenance and attestations](https://docs.kici.dev/user/provenance/): Generate and verify signed SLSA provenance for the artifacts your workflows build
91
92
  - [Secrets](https://docs.kici.dev/user/secrets/): How to access secrets in KiCI workflow steps
@@ -26,15 +26,14 @@ export interface SpawnOrchestratorOptions {
26
26
  * generated by the plane. Enables the orchestrator's local OIDC mint +
27
27
  * dev-signed provenance (issuer `kici-local`) in independent mode. Present in
28
28
  * the independent boot; omitted (with `attach`) in the hybrid boot, which
29
- * mints via the Platform relay instead.
29
+ * mints with the orchestrator's own signing key instead.
30
30
  */
31
31
  devIdentityKeyFile?: string;
32
32
  /**
33
33
  * When present, boot the orchestrator in HYBRID mode attached to the Platform
34
- * relay instead of independent mode. OIDC + attestation then mint via the
35
- * Platform (the local dev signer is never consulted the orchestrator's
36
- * `selectOidcMintRegistration` prefers the relay whenever a Platform
37
- * connection exists).
34
+ * relay instead of independent mode. OIDC + attestation then mint with the
35
+ * orchestrator's own signing key under its own issuer, exactly as a deployed
36
+ * orchestrator does (the local dev signer is never consulted).
38
37
  */
39
38
  attach?: {
40
39
  /** Orchestrator KICI_PLATFORM_URL — a ws(s)://…/ws relay URL. */
@@ -1,5 +1,6 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { planePaths, planePorts } from "./paths.js";
3
+ import { rotatePlaneLogIfOversized } from "./plane-log.js";
3
4
  import { createRequire } from "node:module";
4
5
  import fs from "node:fs";
5
6
  import { spawn } from "node:child_process";
@@ -48,6 +49,7 @@ function spawnOrchestratorProcess(databaseUrl, opts) {
48
49
  const { logFile, root, cacheDir } = planePaths();
49
50
  fs.mkdirSync(root, { recursive: true });
50
51
  fs.mkdirSync(cacheDir, { recursive: true });
52
+ rotatePlaneLogIfOversized(logFile);
51
53
  const out = fs.openSync(logFile, "a");
52
54
  const baseEnv = {};
53
55
  for (const [key, value] of Object.entries(process.env)) if (!key.startsWith("KICI_")) baseEnv[key] = value;
@@ -58,6 +60,7 @@ function spawnOrchestratorProcess(databaseUrl, opts) {
58
60
  KICI_AGENT_AUTH: "none",
59
61
  KICI_HOST: "127.0.0.1",
60
62
  KICI_AUTO_MIGRATE: "true",
63
+ KICI_CLUSTER_SINGLE_NODE: "true",
61
64
  KICI_DATA_DIR: root,
62
65
  KICI_BOOTSTRAP_ADMIN_TOKEN: opts.adminToken,
63
66
  KICI_SECRET_KEY: opts.secretKey,
@@ -74,7 +77,8 @@ function spawnOrchestratorProcess(databaseUrl, opts) {
74
77
  modeEnv = {
75
78
  KICI_MODE: "hybrid",
76
79
  KICI_PLATFORM_URL: opts.attach.platformWsUrl,
77
- KICI_PLATFORM_TOKEN: opts.attach.platformToken
80
+ KICI_PLATFORM_TOKEN: opts.attach.platformToken,
81
+ KICI_ORCHESTRATOR_PROVENANCE_ISSUER: `http://127.0.0.1:${port}`
78
82
  };
79
83
  } else {
80
84
  entry = resolveStandaloneEntry();
@@ -33,6 +33,7 @@ export declare function planePaths(): {
33
33
  pidfile: string;
34
34
  stampFile: string;
35
35
  logFile: string;
36
+ pgLogFile: string;
36
37
  socketDir: string;
37
38
  adminTokenFile: string;
38
39
  platformTokenFile: string;
@@ -48,6 +48,7 @@ function planePaths() {
48
48
  pidfile: path.join(root, "plane.pid"),
49
49
  stampFile: path.join(root, "stamp.json"),
50
50
  logFile: path.join(root, "orchestrator.log"),
51
+ pgLogFile: path.join(root, "orchestrator.log.pg"),
51
52
  socketDir: path.join(root, "sock"),
52
53
  adminTokenFile: path.join(root, "admin-token"),
53
54
  platformTokenFile: path.join(root, "platform-token"),
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Size at which a local dev plane log is rotated on the next plane start.
3
+ *
4
+ * The plane's orchestrator writes one JSON line per request and per
5
+ * scheduled-job tick, and nothing truncates the file between boots, so an
6
+ * unrotated log grows without bound. 50 MB is roughly 200k lines — more history
7
+ * than a local debugging session reads.
8
+ */
9
+ export declare const PLANE_LOG_MAX_BYTES: number;
10
+ /** The cap expressed in whole megabytes, for the messages that name it. */
11
+ export declare const PLANE_LOG_MAX_MB: number;
12
+ /**
13
+ * Rotate `file` to `<file>.1` once it reaches `PLANE_LOG_MAX_BYTES`, keeping two
14
+ * generations: any previous `.1` is discarded.
15
+ *
16
+ * Call this only where nothing holds the file open. A rename under a live
17
+ * writer leaves that writer appending to the renamed inode, so the live log
18
+ * would stay empty for the rest of the process's life.
19
+ *
20
+ * Every failure is swallowed on purpose. Both callers are on a plane-boot path:
21
+ * `startPlanePostgres` reads a throw as "embedded Postgres is unavailable" and
22
+ * silently falls back to a Podman container, and `spawnOrchestratorProcess`
23
+ * would fail the boot outright. A log that cannot be rotated must not cost the
24
+ * user their plane.
25
+ */
26
+ export declare function rotatePlaneLogIfOversized(file: string): void;
27
+ //# sourceMappingURL=plane-log.d.ts.map
@@ -0,0 +1,39 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import fs from "node:fs";
3
+ //#region src/local-plane/plane-log.ts
4
+ /**
5
+ * Size at which a local dev plane log is rotated on the next plane start.
6
+ *
7
+ * The plane's orchestrator writes one JSON line per request and per
8
+ * scheduled-job tick, and nothing truncates the file between boots, so an
9
+ * unrotated log grows without bound. 50 MB is roughly 200k lines — more history
10
+ * than a local debugging session reads.
11
+ */
12
+ const PLANE_LOG_MAX_BYTES = 52428800;
13
+ /** The cap expressed in whole megabytes, for the messages that name it. */
14
+ const PLANE_LOG_MAX_MB = PLANE_LOG_MAX_BYTES / 1048576;
15
+ /**
16
+ * Rotate `file` to `<file>.1` once it reaches `PLANE_LOG_MAX_BYTES`, keeping two
17
+ * generations: any previous `.1` is discarded.
18
+ *
19
+ * Call this only where nothing holds the file open. A rename under a live
20
+ * writer leaves that writer appending to the renamed inode, so the live log
21
+ * would stay empty for the rest of the process's life.
22
+ *
23
+ * Every failure is swallowed on purpose. Both callers are on a plane-boot path:
24
+ * `startPlanePostgres` reads a throw as "embedded Postgres is unavailable" and
25
+ * silently falls back to a Podman container, and `spawnOrchestratorProcess`
26
+ * would fail the boot outright. A log that cannot be rotated must not cost the
27
+ * user their plane.
28
+ */
29
+ function rotatePlaneLogIfOversized(file) {
30
+ try {
31
+ if (fs.statSync(file).size < 52428800) return;
32
+ fs.rmSync(`${file}.1`, { force: true });
33
+ fs.renameSync(file, `${file}.1`);
34
+ } catch {}
35
+ }
36
+ //#endregion
37
+ export { PLANE_LOG_MAX_BYTES, PLANE_LOG_MAX_MB, rotatePlaneLogIfOversized };
38
+
39
+ //# sourceMappingURL=plane-log.js.map
@@ -19,8 +19,8 @@ import { generateKeyPairSync, randomBytes } from "node:crypto";
19
19
  */
20
20
  function currentBuildIdentity() {
21
21
  return {
22
- version: "0.7.0",
23
- buildCommit: "31843a21c"
22
+ version: "0.9.0",
23
+ buildCommit: "69765a7ee"
24
24
  };
25
25
  }
26
26
  /**
@@ -11,6 +11,9 @@
11
11
  * The source's bundle hot-reload is debounced, so a first trigger can land
12
12
  * before the plane has registered the (re-pointed) local source. This resends
13
13
  * the webhook after a grace window until a run appears or the timeout elapses.
14
+ * When the budget still runs out, the timeout is explained rather than merely
15
+ * reported: the plane's own Raft role and its event-log record for this
16
+ * delivery decide which cause is named.
14
17
  */
15
18
  /** A minimal admin-read client (AdminApiClient.get) — injectable for tests. */
16
19
  export interface RunDiscoveryClient {
@@ -58,7 +61,32 @@ export interface TriggerRunOptions {
58
61
  pollIntervalMs?: number;
59
62
  resendAfterMs?: number;
60
63
  timeoutMs?: number;
64
+ /** Workdir the plane's local source points at — named in the lock-file diagnosis. */
65
+ repoBasePath?: string;
66
+ /** Plane log path — named when nothing else explains the timeout. */
67
+ logPath?: string;
61
68
  }
69
+ /** What the diagnosis knows about the trigger that just timed out. */
70
+ export interface TriggerTimeoutContext {
71
+ /** Routing-key-scoped delivery id, or null when the plane never accepted the webhook. */
72
+ deliveryId: string | null;
73
+ orgId: string;
74
+ /** Commit the synthetic push carried (the workdir's overlay commit). */
75
+ sha: string;
76
+ /** Absolute path the plane's local source points at — the run's workdir. */
77
+ repoBasePath?: string;
78
+ /** Plane log path, named in the fallback so the developer has somewhere to look. */
79
+ logPath?: string;
80
+ }
81
+ /**
82
+ * Explain a trigger timeout in one line, from what the plane itself recorded.
83
+ *
84
+ * Ordered most-specific-first, and every branch is a fact read back off the
85
+ * plane rather than an inference: the Raft role it reports, then the status it
86
+ * wrote for THIS delivery, then the delivery id + log path so the developer has
87
+ * a thread to pull even when neither surface answered.
88
+ */
89
+ export declare function diagnoseTriggerTimeout(client: RunDiscoveryClient, ctx: TriggerTimeoutContext): Promise<string>;
62
90
  /**
63
91
  * Trigger the run and resolve its runId. Sends the synthetic push, then polls
64
92
  * the admin runs list filtered by this webhook's routing-key-scoped delivery id
@@ -1,4 +1,5 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
+ import { EventLogStatus } from "@kici-dev/engine";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { AdminApiClient } from "@kici-dev/orchestrator";
4
5
  //#region src/local-plane/plane-trigger.ts
@@ -15,6 +16,9 @@ import { AdminApiClient } from "@kici-dev/orchestrator";
15
16
  * The source's bundle hot-reload is debounced, so a first trigger can land
16
17
  * before the plane has registered the (re-pointed) local source. This resends
17
18
  * the webhook after a grace window until a run appears or the timeout elapses.
19
+ * When the budget still runs out, the timeout is explained rather than merely
20
+ * reported: the plane's own Raft role and its event-log record for this
21
+ * delivery decide which cause is named.
18
22
  */
19
23
  /**
20
24
  * Build the GitHub-shaped webhook request the plane's local provider normalizer
@@ -62,6 +66,50 @@ async function sendLocalTrigger(planeUrl, req) {
62
66
  deliveryId
63
67
  };
64
68
  }
69
+ /** Raft role the plane reports, or null when `/cluster/health` cannot be read. */
70
+ async function readPlaneRole(client) {
71
+ try {
72
+ const health = await client.get("/cluster/health");
73
+ return typeof health.role === "string" ? health.role : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ /** The plane's own record of this delivery, or null when there is none to read. */
79
+ async function readDelivery(client, orgId, deliveryId) {
80
+ try {
81
+ const qs = new URLSearchParams({
82
+ deliveryId,
83
+ orgId,
84
+ limit: "1"
85
+ });
86
+ return (await client.get(`/api/v1/admin/event-log?${qs}`)).deliveries?.[0] ?? null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Explain a trigger timeout in one line, from what the plane itself recorded.
93
+ *
94
+ * Ordered most-specific-first, and every branch is a fact read back off the
95
+ * plane rather than an inference: the Raft role it reports, then the status it
96
+ * wrote for THIS delivery, then the delivery id + log path so the developer has
97
+ * a thread to pull even when neither surface answered.
98
+ */
99
+ async function diagnoseTriggerTimeout(client, ctx) {
100
+ const role = await readPlaneRole(client);
101
+ if (role !== null && role !== "leader") return `plane is not leader yet (election grace period) — it reports Raft role "${role}". A single-node plane should elect itself within seconds; check ${ctx.logPath ?? "the plane log (`kici local logs`)"} for "self-electing as leader".`;
102
+ const delivery = ctx.deliveryId ? await readDelivery(client, ctx.orgId, ctx.deliveryId) : null;
103
+ if (delivery?.status === EventLogStatus.enum.lockfile_missing) {
104
+ const where = ctx.repoBasePath ? ` in ${ctx.repoBasePath}` : "";
105
+ return `no kici.lock.json at ${ctx.sha}${where} — the plane resolved no lock file for this commit, so nothing matched. Make sure .kici/kici.lock.json is committed or present in the working tree the run packs.`;
106
+ }
107
+ if (delivery?.status) {
108
+ const detail = delivery.errorMessage ? ` (${delivery.errorMessage})` : "";
109
+ return `the plane recorded delivery ${ctx.deliveryId} as "${delivery.status}"${detail} but created no run.`;
110
+ }
111
+ return `no run appeared for ${ctx.deliveryId ? `delivery ${ctx.deliveryId}` : "this trigger — the plane never accepted the webhook (no delivery id came back)"}${ctx.logPath ? ` — see ${ctx.logPath}` : ""}`;
112
+ }
65
113
  /**
66
114
  * Trigger the run and resolve its runId. Sends the synthetic push, then polls
67
115
  * the admin runs list filtered by this webhook's routing-key-scoped delivery id
@@ -90,7 +138,14 @@ async function triggerRun(planeUrl, adminToken, input, opts = {}) {
90
138
  }
91
139
  await sleep(pollIntervalMs);
92
140
  }
93
- throw new Error("offline run: no run appeared after triggering the local plane");
141
+ const cause = await diagnoseTriggerTimeout(client, {
142
+ deliveryId,
143
+ orgId: input.orgId,
144
+ sha: input.sha,
145
+ ...opts.repoBasePath !== void 0 && { repoBasePath: opts.repoBasePath },
146
+ ...opts.logPath !== void 0 && { logPath: opts.logPath }
147
+ });
148
+ throw new Error(`offline run: ${cause}`);
94
149
  }
95
150
  /** Return the run created by this webhook delivery, or null when none yet. */
96
151
  async function findRunByDelivery(client, deliveryId) {
@@ -105,6 +160,6 @@ function sleep(ms) {
105
160
  return new Promise((r) => setTimeout(r, ms));
106
161
  }
107
162
  //#endregion
108
- export { buildLocalTriggerRequest, sendLocalTrigger, triggerRun };
163
+ export { buildLocalTriggerRequest, diagnoseTriggerTimeout, sendLocalTrigger, triggerRun };
109
164
 
110
165
  //# sourceMappingURL=plane-trigger.js.map
@@ -1,5 +1,6 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { planePaths, planePorts } from "./paths.js";
3
+ import { rotatePlaneLogIfOversized } from "./plane-log.js";
3
4
  import { createRequire } from "node:module";
4
5
  import path from "node:path";
5
6
  import fs from "node:fs";
@@ -104,14 +105,13 @@ async function embeddedClusterIsServing(port) {
104
105
  /**
105
106
  * Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
106
107
  * this CLI process (embedded-postgres's in-process server is killed by its own
107
- * exit hook, so it cannot back a warm plane). A cluster already serving this
108
- * plane's port is reused as-is.
108
+ * exit hook, so it cannot back a warm plane). The caller decides whether a
109
+ * cluster is already serving; this always starts one.
109
110
  */
110
111
  async function defaultEmbeddedDaemon(port) {
111
- const { pgData, logFile } = planePaths();
112
- if (await embeddedClusterIsServing(port)) return;
112
+ const { pgData, pgLogFile } = planePaths();
113
113
  const pgCtl = resolvePgCtl();
114
- await $`${pgCtl} -D ${pgData} -o ${`-p ${port}`} -l ${`${logFile}.pg`} -w start`.quiet();
114
+ await $`${pgCtl} -D ${pgData} -o ${`-p ${port}`} -l ${pgLogFile} -w start`.quiet();
115
115
  }
116
116
  /** Stop the detached embedded postmaster (handle-independent, reads the data dir). */
117
117
  async function stopEmbeddedDaemon() {
@@ -141,7 +141,10 @@ async function startPlanePostgres(opts = {}) {
141
141
  const url = `postgres://kici:kici@127.0.0.1:${port}/kici_local`;
142
142
  if (!(opts.forcePodman || process.env.KICI_LOCAL_PG_MODE === "podman")) try {
143
143
  await ensureEmbeddedCluster(port);
144
- await embeddedDaemon(port);
144
+ if (!await embeddedClusterIsServing(port)) {
145
+ rotatePlaneLogIfOversized(planePaths().pgLogFile);
146
+ await embeddedDaemon(port);
147
+ }
145
148
  return {
146
149
  url,
147
150
  kind: "embedded",
@@ -1,4 +1,5 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
+ import { unwrapStoredLogLine } from "../remote/output/streaming.js";
2
3
  import { ExecutionJobStatus, ExecutionRunStatus, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES } from "@kici-dev/engine";
3
4
  import { AdminApiClient } from "@kici-dev/orchestrator";
4
5
  //#region src/local-plane/run-follow.ts
@@ -131,7 +132,7 @@ async function drainLogs(client, runId, cursors, onLine) {
131
132
  const qs = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
132
133
  const page = await client.get(`/api/v1/admin/runs/${runId}/jobs/${job.jobId}/steps/${step.stepIndex}/logs${qs}`);
133
134
  for (const l of page.lines) {
134
- onLine(l.value);
135
+ onLine(unwrapStoredLogLine(l.value));
135
136
  emitted++;
136
137
  }
137
138
  cursor = page.nextCursor ?? String(page.totalLines);
@@ -7,6 +7,18 @@
7
7
  * - Prints step transition headers
8
8
  * - Tracks elapsed time with in-place status updates
9
9
  */
10
+ /**
11
+ * The text a stored log line carries.
12
+ *
13
+ * The orchestrator stores every step log line as a JSON envelope —
14
+ * `{"ts":…,"level":"stdout","msg":"…","meta":{}}` — and the Platform relay
15
+ * returns those envelopes verbatim, so a run's log stream is the envelope
16
+ * stream. The dashboard unwraps `msg` before rendering; the terminal must too,
17
+ * or the developer watching `kici run remote` reads raw JSON. A line that is
18
+ * not an envelope (an orchestrator phase marker, a plain line from an older
19
+ * store) passes through unchanged.
20
+ */
21
+ export declare function unwrapStoredLogLine(line: string): string;
10
22
  export declare class StreamingFormatter {
11
23
  /** Color assignment per job name. */
12
24
  private readonly jobColors;