@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
@@ -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);
@@ -2,18 +2,20 @@ import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { compilerError } from "../errors/formatter.js";
3
3
  import { locationForJob, locationForWorkflow } from "../errors/source-location.js";
4
4
  import "../errors/index.js";
5
+ import { computeSiblingsDigest } from "./workspace-siblings.js";
5
6
  import { BREAKING_FLOOR as BREAKING_FLOOR$1, SCHEMA_VERSION as SCHEMA_VERSION$1 } from "../types.js";
6
- import { computeContentHash } from "./hasher.js";
7
+ import { COMPILE_SCHEMA_VERSION, computeContentHash } from "./hasher.js";
7
8
  import { resolveHashFiles } from "./hash-files.js";
8
9
  import path from "node:path";
9
10
  import { readFileSync } from "node:fs";
10
- import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject, normalizeApproval, normalizeCacheSpecs } from "@kici-dev/sdk";
11
+ import { getDynamicJobGitCredentials, getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject } from "@kici-dev/sdk";
11
12
  import { sha256 } from "@kici-dev/core";
12
13
  import { PackageManager, detectPackageManagerSync, detectYarnFlavorSync } from "@kici-dev/core/package-manager";
13
- import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveContentFormat, resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
14
+ import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveContentFormat, resolveWhenToRunOn, stripStatefulRegexFlags, validateResourceRequest } from "@kici-dev/engine";
14
15
  import { assertSafeRegex } from "@kici-dev/engine/safe-regex";
15
- import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFromInput } from "@kici-dev/engine/labels/compile";
16
16
  import { execSync } from "node:child_process";
17
+ import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFromInput } from "@kici-dev/engine/labels/compile";
18
+ import { normalizeApproval, normalizeCacheSpecs } from "@kici-dev/sdk/internal";
17
19
  //#region src/lockfile/generator.ts
18
20
  /**
19
21
  * Courtesy compatibility warning for `kici compile`.
@@ -124,6 +126,7 @@ function formatExportRef(source) {
124
126
  function generateLockFile(workflowsWithSource) {
125
127
  const gitRoot = detectGitRoot();
126
128
  const lockfileHash = computeLockfileHash(gitRoot);
129
+ const siblingsDigest = computeSiblingsDigest(gitRoot);
127
130
  const firstSource = workflowsWithSource[0]?.source;
128
131
  const topLevelSource = firstSource ? {
129
132
  file: path.relative(gitRoot, firstSource.file).replaceAll("\\", "/"),
@@ -147,6 +150,7 @@ function generateLockFile(workflowsWithSource) {
147
150
  source: topLevelSource,
148
151
  contentHash,
149
152
  ...lockfileHash && { lockfileHash },
153
+ ...siblingsDigest && { siblingsDigest },
150
154
  workflows
151
155
  };
152
156
  }
@@ -169,8 +173,8 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
169
173
  resolvedHashFiles = resolved.resolvedPaths;
170
174
  }
171
175
  }
172
- const contentHash = bundleSource !== void 0 ? computeContentHash(bundleSource, 5, assetDigest) : "";
173
- const compileSchemaVersion = bundleSource !== void 0 ? 5 : 0;
176
+ const contentHash = bundleSource !== void 0 ? computeContentHash(bundleSource, COMPILE_SCHEMA_VERSION, assetDigest) : "";
177
+ const compileSchemaVersion = bundleSource !== void 0 ? COMPILE_SCHEMA_VERSION : 0;
174
178
  return {
175
179
  name: workflow.name,
176
180
  source: {
@@ -220,15 +224,25 @@ function toArray(v) {
220
224
  return Array.isArray(v) ? v : [v];
221
225
  }
222
226
  /**
227
+ * The `/pattern/flags` shape the lock file stores.
228
+ *
229
+ * The flag class must name every ECMAScript flag, not just the ones that
230
+ * predate ES2022. When it read `[gimsuy]`, a `d`- or `v`-flagged regex failed
231
+ * to unwrap, so `pattern` became the whole `/release-\d+/v` string — which
232
+ * `new RegExp` happily accepts as a pattern matching literal slashes. The lock
233
+ * then stored `/\/release-\d+\/v/`, which compiles green and can never match.
234
+ */
235
+ const REGEX_ENTRY = /^\/(.+)\/([dgimsuvy]*)$/;
236
+ /**
223
237
  * Normalize one regex entry to the lock's `/pattern/flags` form, rejecting an
224
238
  * invalid or ReDoS-prone pattern at compile time so a catastrophic pattern fails
225
239
  * `kici compile` with author feedback instead of reaching the orchestrator.
226
240
  */
227
241
  function serializeRegexEntry(entry, ctx) {
228
242
  const source = entry instanceof RegExp ? `/${entry.source}/${entry.flags}` : entry;
229
- const wrapped = /^\/(.+)\/([gimsuy]*)$/.exec(source);
243
+ const wrapped = REGEX_ENTRY.exec(source);
230
244
  const pattern = wrapped ? wrapped[1] : source;
231
- const flags = wrapped ? wrapped[2] : "";
245
+ const flags = stripStatefulRegexFlags(wrapped ? wrapped[2] : "");
232
246
  let re;
233
247
  try {
234
248
  re = new RegExp(pattern, flags);
@@ -605,6 +619,7 @@ function transformJobs(jobs, configPath, gitRoot) {
605
619
  if (isDynamicJobFn(jobOrFactory)) {
606
620
  const groupName = getDynamicJobGroup(jobOrFactory);
607
621
  const declaredNeeds = getDynamicJobNeeds(jobOrFactory);
622
+ const declaredGitCredentials = getDynamicJobGitCredentials(jobOrFactory);
608
623
  const resolvedNeeds = declaredNeeds ? resolveNeedsForLock(declaredNeeds, uuidToName) : void 0;
609
624
  return {
610
625
  _type: "dynamic",
@@ -616,7 +631,8 @@ function transformJobs(jobs, configPath, gitRoot) {
616
631
  ...resolvedNeeds && {
617
632
  needs: resolvedNeeds.needs,
618
633
  resultAware: true
619
- }
634
+ },
635
+ ...declaredGitCredentials && { gitCredentials: declaredGitCredentials }
620
636
  };
621
637
  }
622
638
  const j = jobOrFactory;
@@ -1,18 +1,10 @@
1
1
  /**
2
- * Compile schema version -- bump when compilation approach changes
3
- * (bundler change, bundling config, output format, etc.).
4
- * This is NOT the lock file schema version.
5
- *
6
- * Bumped 3 → 4: the agent artifact switched from a Rolldown-bundled
7
- * `.compiled.mjs` to a raw `.kici/` source tarball that the agent extracts
8
- * and imports via the shared oxc-transform ESM loader hook.
9
- *
10
- * Bumped 4 → 5: the hash input is now line-ending-normalized (CRLF → LF) so a
11
- * lockfile produced on Linux (LF) matches the agent's hash on Windows, where
12
- * Git's `core.autocrlf=true` system default rewrites checked-out text files to
13
- * CRLF. Old lockfiles must be regenerated via `kici compile`.
2
+ * Compile schema version, re-exported from its single definition in
3
+ * `@kici-dev/core/kici-source-digest` where it sits beside the digest it
4
+ * qualifies, so the compiler that writes a `contentHash` and the agent that
5
+ * re-verifies it cannot drift onto different numbers.
14
6
  */
15
- export declare const COMPILE_SCHEMA_VERSION = 5;
7
+ export { COMPILE_SCHEMA_VERSION } from '@kici-dev/core/kici-source-digest';
16
8
  /**
17
9
  * Compute content hash for a compiled workflow bundle, optionally including asset file contents.
18
10
  * Hash = SHA-256(schemaVersion + ":" + bundleSource + "\0" + assetDigest) when assetDigest is provided,
@@ -1,22 +1,8 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
+ import { COMPILE_SCHEMA_VERSION } from "@kici-dev/core/kici-source-digest";
2
3
  import { normalizeLineEndings, sha256 } from "@kici-dev/core";
3
4
  //#region src/lockfile/hasher.ts
4
5
  /**
5
- * Compile schema version -- bump when compilation approach changes
6
- * (bundler change, bundling config, output format, etc.).
7
- * This is NOT the lock file schema version.
8
- *
9
- * Bumped 3 → 4: the agent artifact switched from a Rolldown-bundled
10
- * `.compiled.mjs` to a raw `.kici/` source tarball that the agent extracts
11
- * and imports via the shared oxc-transform ESM loader hook.
12
- *
13
- * Bumped 4 → 5: the hash input is now line-ending-normalized (CRLF → LF) so a
14
- * lockfile produced on Linux (LF) matches the agent's hash on Windows, where
15
- * Git's `core.autocrlf=true` system default rewrites checked-out text files to
16
- * CRLF. Old lockfiles must be regenerated via `kici compile`.
17
- */
18
- const COMPILE_SCHEMA_VERSION = 5;
19
- /**
20
6
  * Compute content hash for a compiled workflow bundle, optionally including asset file contents.
21
7
  * Hash = SHA-256(schemaVersion + ":" + bundleSource + "\0" + assetDigest) when assetDigest is provided,
22
8
  * otherwise SHA-256(schemaVersion + ":" + bundleSource) for backward compatibility.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Static resolution of the in-repo `workspace:` sibling closure `.kici` depends
3
+ * on, and a digest over its source.
4
+ *
5
+ * The deps tarball carries those sibling package directories **with their built
6
+ * output** (`packKiciDeps`), while the pointer that names it is keyed on
7
+ * `lockfileHash` — a hash of the package-manager lock file. Editing a sibling's
8
+ * source moves no package-manager lock file, so a repo whose `.kici` depends on
9
+ * an in-repo `workspace:` package restored that sibling's STALE build over the
10
+ * fresh clone on every warm-cache run. `siblingsDigest` closes that: it enters
11
+ * the dep pointer key, so a sibling edit is a pointer miss and the deps are
12
+ * rebuilt.
13
+ *
14
+ * Resolution is **static** — manifests and `git ls-files`, never an installed
15
+ * `node_modules`. The agent's own `collectInRepoSiblings` walks an installed
16
+ * tree, which cannot be used here: `kici compile` would then emit a different
17
+ * lock file before and after an install, and a lock file whose contents depend
18
+ * on whether you have installed is not hermetic.
19
+ */
20
+ /**
21
+ * The workspace globs declared at the repo root — `pnpm-workspace.yaml`'s
22
+ * `packages:` first, then `package.json#workspaces` (npm / yarn / bun). Empty
23
+ * when the repo declares no workspace, in which case a `workspace:` specifier
24
+ * resolves to nothing and is skipped rather than guessed at.
25
+ */
26
+ export declare function readWorkspaceGlobs(gitRoot: string): string[];
27
+ /**
28
+ * Every in-repo sibling directory `.kici` depends on, transitively, as sorted
29
+ * repo-relative POSIX paths.
30
+ *
31
+ * `.kici` itself is excluded: its own source is already covered by
32
+ * `contentHash`, and including it here would make a `.kici` edit invalidate the
33
+ * dep cache for no reason.
34
+ */
35
+ export declare function collectInRepoSiblings(gitRoot: string, kiciDir?: string): string[];
36
+ /**
37
+ * SHA-256 over the git-tracked source of every resolved sibling directory, or
38
+ * null when `.kici` depends on none.
39
+ *
40
+ * `git ls-files` is install-independent and is already the compiler's world (it
41
+ * resolves `gitRoot` to generate the lock at all). Content is read as text and
42
+ * `\0`-delimited against its path, exactly as the `.kici/` tree digest is, so a
43
+ * rename cannot be disguised as a content edit.
44
+ */
45
+ export declare function computeSiblingsDigest(gitRoot: string, kiciDir?: string): string | null;
46
+ //# sourceMappingURL=workspace-siblings.d.ts.map
@@ -0,0 +1,197 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import path from "node:path";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { sha256 } from "@kici-dev/core";
5
+ import { execFileSync } from "node:child_process";
6
+ import { parse } from "yaml";
7
+ //#region src/lockfile/workspace-siblings.ts
8
+ /**
9
+ * Static resolution of the in-repo `workspace:` sibling closure `.kici` depends
10
+ * on, and a digest over its source.
11
+ *
12
+ * The deps tarball carries those sibling package directories **with their built
13
+ * output** (`packKiciDeps`), while the pointer that names it is keyed on
14
+ * `lockfileHash` — a hash of the package-manager lock file. Editing a sibling's
15
+ * source moves no package-manager lock file, so a repo whose `.kici` depends on
16
+ * an in-repo `workspace:` package restored that sibling's STALE build over the
17
+ * fresh clone on every warm-cache run. `siblingsDigest` closes that: it enters
18
+ * the dep pointer key, so a sibling edit is a pointer miss and the deps are
19
+ * rebuilt.
20
+ *
21
+ * Resolution is **static** — manifests and `git ls-files`, never an installed
22
+ * `node_modules`. The agent's own `collectInRepoSiblings` walks an installed
23
+ * tree, which cannot be used here: `kici compile` would then emit a different
24
+ * lock file before and after an install, and a lock file whose contents depend
25
+ * on whether you have installed is not hermetic.
26
+ */
27
+ /** Dependency protocols that name a directory inside this repository. */
28
+ const IN_REPO_PROTOCOLS = [
29
+ "workspace:",
30
+ "file:",
31
+ "link:",
32
+ "portal:"
33
+ ];
34
+ function readManifest(dir) {
35
+ const file = path.join(dir, "package.json");
36
+ if (!existsSync(file)) return null;
37
+ try {
38
+ return JSON.parse(readFileSync(file, "utf-8"));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ /** Every dependency specifier in a manifest, across all four dependency maps. */
44
+ function allSpecifiers(m) {
45
+ return Object.entries({
46
+ ...m.dependencies,
47
+ ...m.devDependencies,
48
+ ...m.optionalDependencies,
49
+ ...m.peerDependencies
50
+ });
51
+ }
52
+ /**
53
+ * The workspace globs declared at the repo root — `pnpm-workspace.yaml`'s
54
+ * `packages:` first, then `package.json#workspaces` (npm / yarn / bun). Empty
55
+ * when the repo declares no workspace, in which case a `workspace:` specifier
56
+ * resolves to nothing and is skipped rather than guessed at.
57
+ */
58
+ function readWorkspaceGlobs(gitRoot) {
59
+ const pnpmFile = path.join(gitRoot, "pnpm-workspace.yaml");
60
+ if (existsSync(pnpmFile)) try {
61
+ const doc = parse(readFileSync(pnpmFile, "utf-8"));
62
+ if (Array.isArray(doc?.packages)) return doc.packages.filter((p) => typeof p === "string");
63
+ } catch {}
64
+ const ws = readManifest(gitRoot)?.workspaces;
65
+ if (Array.isArray(ws)) return ws;
66
+ if (ws && Array.isArray(ws.packages)) return ws.packages;
67
+ return [];
68
+ }
69
+ /**
70
+ * Directories the workspace globs match, each carrying a manifest. Resolved with
71
+ * `git ls-files` so the set is exactly what is tracked — the same authority the
72
+ * digest itself uses, and one that needs no install.
73
+ */
74
+ function workspacePackageDirs(gitRoot, globs) {
75
+ const byName = /* @__PURE__ */ new Map();
76
+ if (globs.length === 0) return byName;
77
+ let manifests;
78
+ try {
79
+ manifests = execFileSync("git", [
80
+ "ls-files",
81
+ "--",
82
+ "*package.json",
83
+ "package.json"
84
+ ], {
85
+ cwd: gitRoot,
86
+ encoding: "utf-8",
87
+ maxBuffer: 33554432
88
+ }).split("\n").filter(Boolean);
89
+ } catch {
90
+ return byName;
91
+ }
92
+ for (const rel of manifests) {
93
+ const dir = path.dirname(rel);
94
+ if (dir.split("/").includes("node_modules")) continue;
95
+ const m = readManifest(path.join(gitRoot, dir));
96
+ if (m?.name) byName.set(m.name, dir);
97
+ }
98
+ return byName;
99
+ }
100
+ /**
101
+ * Resolve one specifier to a repo-relative directory, or null when it does not
102
+ * name one. `file:` / `link:` / `portal:` carry the path; `workspace:` carries a
103
+ * version range, so the package NAME is resolved through the workspace globs.
104
+ */
105
+ function resolveSpecifier(pkgName, spec, fromDir, gitRoot, workspacePkgs) {
106
+ if (spec.startsWith("workspace:")) return workspacePkgs.get(pkgName) ?? null;
107
+ for (const proto of [
108
+ "file:",
109
+ "link:",
110
+ "portal:"
111
+ ]) {
112
+ if (!spec.startsWith(proto)) continue;
113
+ const target = spec.slice(proto.length);
114
+ const abs = path.resolve(gitRoot, fromDir, target);
115
+ const rel = path.relative(gitRoot, abs).replaceAll("\\", "/");
116
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
117
+ return rel;
118
+ }
119
+ return null;
120
+ }
121
+ /**
122
+ * Every in-repo sibling directory `.kici` depends on, transitively, as sorted
123
+ * repo-relative POSIX paths.
124
+ *
125
+ * `.kici` itself is excluded: its own source is already covered by
126
+ * `contentHash`, and including it here would make a `.kici` edit invalidate the
127
+ * dep cache for no reason.
128
+ */
129
+ function collectInRepoSiblings(gitRoot, kiciDir = ".kici") {
130
+ const kiciManifest = readManifest(path.join(gitRoot, kiciDir));
131
+ if (!kiciManifest) return [];
132
+ const workspacePkgs = workspacePackageDirs(gitRoot, readWorkspaceGlobs(gitRoot));
133
+ const found = /* @__PURE__ */ new Set();
134
+ const queue = [{
135
+ dir: kiciDir,
136
+ manifest: kiciManifest
137
+ }];
138
+ const visited = /* @__PURE__ */ new Set([kiciDir]);
139
+ while (queue.length > 0) {
140
+ const { dir, manifest } = queue.shift();
141
+ for (const [name, spec] of allSpecifiers(manifest)) {
142
+ if (!IN_REPO_PROTOCOLS.some((p) => spec.startsWith(p))) continue;
143
+ const target = resolveSpecifier(name, spec, dir, gitRoot, workspacePkgs);
144
+ if (target === null || visited.has(target)) continue;
145
+ visited.add(target);
146
+ found.add(target);
147
+ const m = readManifest(path.join(gitRoot, target));
148
+ if (m) queue.push({
149
+ dir: target,
150
+ manifest: m
151
+ });
152
+ }
153
+ }
154
+ return [...found].sort();
155
+ }
156
+ /**
157
+ * SHA-256 over the git-tracked source of every resolved sibling directory, or
158
+ * null when `.kici` depends on none.
159
+ *
160
+ * `git ls-files` is install-independent and is already the compiler's world (it
161
+ * resolves `gitRoot` to generate the lock at all). Content is read as text and
162
+ * `\0`-delimited against its path, exactly as the `.kici/` tree digest is, so a
163
+ * rename cannot be disguised as a content edit.
164
+ */
165
+ function computeSiblingsDigest(gitRoot, kiciDir = ".kici") {
166
+ const siblings = collectInRepoSiblings(gitRoot, kiciDir);
167
+ if (siblings.length === 0) return null;
168
+ const parts = [];
169
+ for (const dir of siblings) {
170
+ let files;
171
+ try {
172
+ files = execFileSync("git", [
173
+ "ls-files",
174
+ "--",
175
+ dir
176
+ ], {
177
+ cwd: gitRoot,
178
+ encoding: "utf-8",
179
+ maxBuffer: 67108864
180
+ }).split("\n").filter(Boolean).sort();
181
+ } catch {
182
+ continue;
183
+ }
184
+ for (const rel of files) {
185
+ let content = "";
186
+ try {
187
+ content = readFileSync(path.join(gitRoot, rel), "utf-8");
188
+ } catch {}
189
+ parts.push(`${rel}\0${content.replaceAll("\r\n", "\n")}\0`);
190
+ }
191
+ }
192
+ return parts.length > 0 ? sha256(parts.join("")) : null;
193
+ }
194
+ //#endregion
195
+ export { collectInRepoSiblings, computeSiblingsDigest, readWorkspaceGlobs };
196
+
197
+ //# sourceMappingURL=workspace-siblings.js.map
@@ -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;
@@ -19,6 +19,25 @@ const COLOR_PALETTE = [
19
19
  pc.magenta,
20
20
  pc.cyan
21
21
  ];
22
+ /**
23
+ * The text a stored log line carries.
24
+ *
25
+ * The orchestrator stores every step log line as a JSON envelope —
26
+ * `{"ts":…,"level":"stdout","msg":"…","meta":{}}` — and the Platform relay
27
+ * returns those envelopes verbatim, so a run's log stream is the envelope
28
+ * stream. The dashboard unwraps `msg` before rendering; the terminal must too,
29
+ * or the developer watching `kici run remote` reads raw JSON. A line that is
30
+ * not an envelope (an orchestrator phase marker, a plain line from an older
31
+ * store) passes through unchanged.
32
+ */
33
+ function unwrapStoredLogLine(line) {
34
+ if (!line.startsWith("{")) return line;
35
+ try {
36
+ const parsed = JSON.parse(line);
37
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.msg === "string") return parsed.msg;
38
+ } catch {}
39
+ return line;
40
+ }
22
41
  var StreamingFormatter = class {
23
42
  /** Color assignment per job name. */
24
43
  jobColors = /* @__PURE__ */ new Map();
@@ -119,6 +138,6 @@ var StreamingFormatter = class {
119
138
  }
120
139
  };
121
140
  //#endregion
122
- export { StreamingFormatter };
141
+ export { StreamingFormatter, unwrapStoredLogLine };
123
142
 
124
143
  //# sourceMappingURL=streaming.js.map
@@ -106,6 +106,8 @@ export interface PlatformRunStatusResponse {
106
106
  status: string;
107
107
  exitCode?: number | null;
108
108
  errorMessage?: string | null;
109
+ /** Absent or null from an orchestrator that does not report it. */
110
+ durationMs?: number | null;
109
111
  }>;
110
112
  done: boolean;
111
113
  }