@kici-dev/compiler 0.1.27 → 0.3.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 (153) hide show
  1. package/dist/auth/headless-detect.d.ts +14 -5
  2. package/dist/auth/headless-detect.js +50 -9
  3. package/dist/cli.js +120 -11
  4. package/dist/commands/compile.js +28 -4
  5. package/dist/commands/diagnostics.js +14 -2
  6. package/dist/commands/doctor.d.ts +64 -0
  7. package/dist/commands/doctor.js +413 -0
  8. package/dist/commands/endpoints.js +6 -0
  9. package/dist/commands/fixture.js +13 -0
  10. package/dist/commands/hook.js +3 -2
  11. package/dist/commands/index.d.ts +8 -0
  12. package/dist/commands/index.js +5 -1
  13. package/dist/commands/init-host-os.d.ts +34 -0
  14. package/dist/commands/init-host-os.js +44 -0
  15. package/dist/commands/init.d.ts +4 -0
  16. package/dist/commands/init.js +159 -36
  17. package/dist/commands/local.d.ts +28 -2
  18. package/dist/commands/local.js +74 -5
  19. package/dist/commands/login.js +1 -1
  20. package/dist/commands/notifications.d.ts +61 -0
  21. package/dist/commands/notifications.js +274 -0
  22. package/dist/commands/preview.js +8 -1
  23. package/dist/commands/run-routed.js +10 -1
  24. package/dist/commands/run.js +41 -12
  25. package/dist/commands/runs/artifacts/download.d.ts +24 -0
  26. package/dist/commands/runs/artifacts/download.js +154 -0
  27. package/dist/commands/runs/artifacts/list.d.ts +5 -0
  28. package/dist/commands/runs/artifacts/list.js +47 -0
  29. package/dist/commands/runs/cancel.js +2 -2
  30. package/dist/commands/runs/list.js +8 -2
  31. package/dist/commands/verify-attestation.d.ts +3 -1
  32. package/dist/commands/verify-attestation.js +18 -7
  33. package/dist/errors/formatter.d.ts +14 -1
  34. package/dist/errors/formatter.js +13 -3
  35. package/dist/errors/index.d.ts +2 -1
  36. package/dist/errors/index.js +3 -2
  37. package/dist/errors/source-location.d.ts +20 -0
  38. package/dist/errors/source-location.js +45 -0
  39. package/dist/execution/executor.js +25 -5
  40. package/dist/fixtures/defaults/index.js +2 -1
  41. package/dist/llm-context/llms-architecture.txt +51 -22
  42. package/dist/llm-context/llms-cli.txt +2461 -1582
  43. package/dist/llm-context/llms-features.txt +178 -106
  44. package/dist/llm-context/llms-full.txt +6127 -4426
  45. package/dist/llm-context/llms-getting-started.txt +306 -42
  46. package/dist/llm-context/llms-patterns.txt +13 -7
  47. package/dist/llm-context/llms-sdk-runtime.txt +2013 -0
  48. package/dist/llm-context/llms-sdk.txt +591 -2152
  49. package/dist/llm-context/llms.txt +30 -16
  50. package/dist/local-plane/orchestrator-process.d.ts +12 -7
  51. package/dist/local-plane/orchestrator-process.js +28 -14
  52. package/dist/local-plane/os-mismatch-hint.d.ts +16 -0
  53. package/dist/local-plane/os-mismatch-hint.js +34 -0
  54. package/dist/local-plane/plane-liveness.d.ts +68 -0
  55. package/dist/local-plane/plane-liveness.js +145 -0
  56. package/dist/local-plane/plane-manager.d.ts +60 -18
  57. package/dist/local-plane/plane-manager.js +210 -70
  58. package/dist/local-plane/plane-trigger.d.ts +13 -4
  59. package/dist/local-plane/plane-trigger.js +27 -14
  60. package/dist/local-plane/platform-attach.js +1 -5
  61. package/dist/local-plane/port-holder.d.ts +78 -0
  62. package/dist/local-plane/port-holder.js +227 -0
  63. package/dist/local-plane/postgres.d.ts +15 -3
  64. package/dist/local-plane/postgres.js +34 -12
  65. package/dist/local-plane/run-follow.d.ts +20 -0
  66. package/dist/local-plane/run-follow.js +51 -4
  67. package/dist/local-plane/scaler-config.d.ts +28 -14
  68. package/dist/local-plane/scaler-config.js +59 -41
  69. package/dist/local-plane/source-provider.js +4 -10
  70. package/dist/lockfile/generator.d.ts +14 -1
  71. package/dist/lockfile/generator.js +99 -33
  72. package/dist/lockfile/index.d.ts +3 -1
  73. package/dist/lockfile/index.js +3 -2
  74. package/dist/lockfile/purity-diagnostics.d.ts +31 -0
  75. package/dist/lockfile/purity-diagnostics.js +52 -0
  76. package/dist/postinstall.js +2 -1
  77. package/dist/remote/artifact-extract.d.ts +8 -0
  78. package/dist/remote/artifact-extract.js +58 -0
  79. package/dist/remote/config.d.ts +21 -3
  80. package/dist/remote/config.js +24 -5
  81. package/dist/remote/dashboard-client.d.ts +39 -2
  82. package/dist/remote/dashboard-client.js +47 -4
  83. package/dist/remote/fs-case.d.ts +21 -0
  84. package/dist/remote/fs-case.js +45 -0
  85. package/dist/remote/history.d.ts +9 -3
  86. package/dist/remote/history.js +79 -10
  87. package/dist/remote/local-repo-identity.d.ts +1 -1
  88. package/dist/remote/local-repo-identity.js +2 -1
  89. package/dist/remote/notifications-client.d.ts +149 -0
  90. package/dist/remote/notifications-client.js +103 -0
  91. package/dist/remote/oauth.d.ts +13 -0
  92. package/dist/remote/oauth.js +31 -5
  93. package/dist/remote/output/summary.js +76 -14
  94. package/dist/remote/render.js +30 -9
  95. package/dist/remote/uploader.js +5 -4
  96. package/dist/templates/agents-md.d.ts +1 -1
  97. package/dist/templates/agents-md.js +6 -6
  98. package/dist/templates/index.d.ts +1 -1
  99. package/dist/templates/index.js +2 -2
  100. package/dist/templates/package-json.d.ts +15 -0
  101. package/dist/templates/package-json.js +26 -5
  102. package/dist/test-runner/dry-run.d.ts +2 -1
  103. package/dist/test-runner/dry-run.js +18 -1
  104. package/dist/test-runner/event-types.d.ts +2 -0
  105. package/dist/test-runner/event-types.js +3 -0
  106. package/dist/test-runner/job-executor.d.ts +1 -0
  107. package/dist/test-runner/job-executor.js +81 -43
  108. package/dist/test-runner/output-formatter.d.ts +5 -2
  109. package/dist/test-runner/output-formatter.js +6 -4
  110. package/dist/test-runner/payload-builder.d.ts +7 -1
  111. package/dist/test-runner/payload-builder.js +11 -2
  112. package/dist/test-runner/rule-evaluator.d.ts +7 -3
  113. package/dist/test-runner/rule-evaluator.js +13 -12
  114. package/dist/test-runner/step-context.d.ts +23 -5
  115. package/dist/test-runner/step-context.js +28 -118
  116. package/dist/types.d.ts +21 -2
  117. package/dist/types.js +4 -2
  118. package/dist/validation/index.d.ts +2 -0
  119. package/dist/validation/index.js +2 -1
  120. package/dist/validation/typecheck.d.ts +28 -0
  121. package/dist/validation/typecheck.js +110 -0
  122. package/dist/validation/validator.d.ts +9 -4
  123. package/dist/validation/validator.js +38 -44
  124. package/package.json +12 -15
  125. package/sbom.spdx.json +1322 -1964
  126. package/dist/assets/api-TJJVHrjC.json +0 -118
  127. package/dist/assets/descriptor-BTtjzN9L.json +0 -1382
  128. package/dist/assets/package-BpQF9kR8.json +0 -74
  129. package/dist/assets/package-Ceo2h27X.json +0 -89
  130. package/dist/assets/source_context-D0atuL28.json +0 -20
  131. package/dist/assets/type-BFqO8SCZ.json +0 -202
  132. package/dist/commands/cancel.d.ts +0 -22
  133. package/dist/commands/detect-package-manager.d.ts +0 -42
  134. package/dist/commands/held-run-resolve.d.ts +0 -50
  135. package/dist/commands/status.d.ts +0 -34
  136. package/dist/commands/test.d.ts +0 -88
  137. package/dist/errors/codes.d.ts +0 -25
  138. package/dist/local-executor/dag-scheduler.d.ts +0 -44
  139. package/dist/local-executor/index.d.ts +0 -23
  140. package/dist/local-executor/job-runner.d.ts +0 -47
  141. package/dist/local-executor/materializer.d.ts +0 -44
  142. package/dist/local-executor/output-streamer.d.ts +0 -31
  143. package/dist/local-executor/payload-generator.d.ts +0 -16
  144. package/dist/local-executor/picker.d.ts +0 -33
  145. package/dist/local-executor/runs-on-display.d.ts +0 -9
  146. package/dist/local-executor/secret-loader.d.ts +0 -18
  147. package/dist/local-executor/to-event-payload.d.ts +0 -16
  148. package/dist/local-executor/types.d.ts +0 -93
  149. package/dist/local-executor/workflow-lock.d.ts +0 -82
  150. package/dist/package-F7UXSDHW.json +0 -74
  151. package/dist/remote/client.d.ts +0 -210
  152. package/dist/remote/observer.d.ts +0 -81
  153. package/dist/test-runner/summary.d.ts +0 -7
@@ -3,8 +3,7 @@ import { selectOverlayFiles } from "../remote/uploader.js";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs/promises";
5
5
  import { execFileSync, execSync } from "node:child_process";
6
- import os from "node:os";
7
- import { randomBytes } from "node:crypto";
6
+ import { makeTempDir } from "@kici-dev/core/tmp";
8
7
  //#region src/local-plane/source-provider.ts
9
8
  /**
10
9
  * LocalSourceProvider — resolve the workdir a `kici run --local` dispatch
@@ -59,8 +58,8 @@ function resolveInPlace(repoRoot) {
59
58
  * carries the work.
60
59
  */
61
60
  async function resolveIsolated(repoRoot) {
62
- const base = os.tmpdir();
63
- const tmpDir = path.join(base, `kici-local-run-${randomBytes(3).toString("hex")}`);
61
+ const workdir = await makeTempDir("local-run", { persist: true });
62
+ const tmpDir = workdir.path;
64
63
  const { sha, existingFiles, deletedFiles } = await selectOverlayFiles(repoRoot);
65
64
  execSync(`git clone --no-hardlinks --quiet ${shellQuote(repoRoot)} ${shellQuote(tmpDir)}`, { stdio: "ignore" });
66
65
  execSync(`git checkout --quiet ${sha}`, {
@@ -100,12 +99,7 @@ async function resolveIsolated(repoRoot) {
100
99
  ref: `refs/heads/${LOCAL_RUN_BRANCH}`,
101
100
  sha: committed,
102
101
  branch: LOCAL_RUN_BRANCH,
103
- cleanup: async () => {
104
- await fs.rm(tmpDir, {
105
- recursive: true,
106
- force: true
107
- });
108
- }
102
+ cleanup: () => workdir.cleanup()
109
103
  };
110
104
  }
111
105
  /** Copy overlay files onto the clone and remove local deletions. */
@@ -1,5 +1,18 @@
1
1
  import type { StepInput, TriggerConfig } from '@kici-dev/sdk';
2
2
  import { type LockFile, type LockTrigger, type LockStepEntry, type WorkflowWithSource } from '../types.js';
3
+ import { type SourceLocation } from '../errors/index.js';
4
+ /**
5
+ * Courtesy compatibility warning for `kici compile`.
6
+ *
7
+ * When the current schema version is itself a breaking version
8
+ * (`floor === version`), locks emitted now stamp `minReaderVersion = version`
9
+ * and cannot be read by orchestrators older than that version — return a
10
+ * one-line heads-up naming the required orchestrator schema. When the floor sits
11
+ * below the current version (`floor < version`) the emitted lock is additive
12
+ * over older readers down to the floor, so no warning is warranted (return
13
+ * null). The orchestrator remains the authoritative reject; this is informational.
14
+ */
15
+ export declare function schemaWindowWarning(floor: number, version: number): string | null;
3
16
  /**
4
17
  * Detect git repository root by running `git rev-parse --show-toplevel`.
5
18
  * Falls back to cwd if not in a git repo.
@@ -47,7 +60,7 @@ export declare function transformTriggers(triggers?: TriggerConfig[]): readonly
47
60
  * compiler's naming matches the agent's `extractAndNormalizeSteps` enumeration —
48
61
  * the flat-stepIndex invariant.
49
62
  */
50
- export declare function transformSteps(steps: readonly StepInput[], gitRoot: string): readonly LockStepEntry[];
63
+ export declare function transformSteps(steps: readonly StepInput[], gitRoot: string, jobLocation?: SourceLocation): readonly LockStepEntry[];
51
64
  /**
52
65
  * Serialize lock file to JSON string.
53
66
  *
@@ -1,5 +1,8 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
- import { SCHEMA_VERSION as SCHEMA_VERSION$1 } from "../types.js";
2
+ import { compilerError } from "../errors/formatter.js";
3
+ import { locationForJob, locationForWorkflow } from "../errors/source-location.js";
4
+ import "../errors/index.js";
5
+ import { BREAKING_FLOOR as BREAKING_FLOOR$1, SCHEMA_VERSION as SCHEMA_VERSION$1 } from "../types.js";
3
6
  import { computeContentHash } from "./hasher.js";
4
7
  import { resolveHashFiles } from "./hash-files.js";
5
8
  import { analyzePurity } from "./purity-analyzer.js";
@@ -13,6 +16,21 @@ import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFrom
13
16
  import { execSync } from "node:child_process";
14
17
  //#region src/lockfile/generator.ts
15
18
  /**
19
+ * Courtesy compatibility warning for `kici compile`.
20
+ *
21
+ * When the current schema version is itself a breaking version
22
+ * (`floor === version`), locks emitted now stamp `minReaderVersion = version`
23
+ * and cannot be read by orchestrators older than that version — return a
24
+ * one-line heads-up naming the required orchestrator schema. When the floor sits
25
+ * below the current version (`floor < version`) the emitted lock is additive
26
+ * over older readers down to the floor, so no warning is warranted (return
27
+ * null). The orchestrator remains the authoritative reject; this is informational.
28
+ */
29
+ function schemaWindowWarning(floor, version) {
30
+ if (floor < version) return null;
31
+ return `This lock uses schema v${version}, a breaking schema version — orchestrators older than v${version} cannot read it. Upgrade the orchestrator to schema v${version} or newer before it can dispatch from this lock.`;
32
+ }
33
+ /**
16
34
  * Detect git repository root by running `git rev-parse --show-toplevel`.
17
35
  * Falls back to cwd if not in a git repo.
18
36
  *
@@ -96,9 +114,11 @@ function generateLockFile(workflowsWithSource) {
96
114
  });
97
115
  return {
98
116
  schemaVersion: SCHEMA_VERSION$1,
117
+ minReaderVersion: BREAKING_FLOOR$1,
99
118
  source: topLevelSource,
100
119
  contentHash: sha256(JSON.stringify({
101
120
  schemaVersion: SCHEMA_VERSION$1,
121
+ minReaderVersion: BREAKING_FLOOR$1,
102
122
  source: topLevelSource,
103
123
  workflows
104
124
  })),
@@ -158,7 +178,7 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
158
178
  ...workflow.concurrency.max !== void 0 && { max: workflow.concurrency.max }
159
179
  } },
160
180
  ...workflow.timeout !== void 0 && { timeout: workflow.timeout },
161
- ...workflow.approval !== void 0 && { approval: (assertNonStepApprovalScope(workflow.approval, "workflow"), toLockApproval(workflow.approval)) }
181
+ ...workflow.approval !== void 0 && { approval: (assertNonStepApprovalScope(workflow.approval, "workflow", locationForWorkflow(sourceFile)), toLockApproval(workflow.approval)) }
162
182
  };
163
183
  }
164
184
  /**
@@ -316,6 +336,14 @@ function toLockWorkflowComplete(t) {
316
336
  ...t.source !== void 0 && { source: t.source }
317
337
  };
318
338
  }
339
+ function toLockWorkflowsFailedBatch(t) {
340
+ return {
341
+ _type: "workflows_failed_batch",
342
+ accumulateFor: t.accumulateFor,
343
+ ...t.name !== void 0 && { name: t.name },
344
+ ...t.source !== void 0 && { source: t.source }
345
+ };
346
+ }
319
347
  function toLockJobComplete(t) {
320
348
  return {
321
349
  _type: "job_complete",
@@ -383,6 +411,7 @@ function transformOneTrigger(trigger) {
383
411
  case "WebhookTrigger": return [toLockWebhook(trigger)];
384
412
  case "KiciEventTrigger": return [toLockKiciEvent(trigger)];
385
413
  case "WorkflowCompleteTrigger": return [toLockWorkflowComplete(trigger)];
414
+ case "WorkflowsFailedBatchTrigger": return [toLockWorkflowsFailedBatch(trigger)];
386
415
  case "JobCompleteTrigger": return [toLockJobComplete(trigger)];
387
416
  case "GenericWebhookTrigger": return [toLockGenericWebhook(trigger)];
388
417
  case "ScheduleTrigger": return [toLockSchedule(trigger)];
@@ -478,11 +507,14 @@ function normalizeRunsOnForLock(runsOn, jobName) {
478
507
  * The overlap check compares exact matchers only — a glob/regex include and an
479
508
  * exact exclude (or vice versa) cannot be statically known to overlap.
480
509
  */
481
- function validateRunsOn(runsOn, jobName) {
510
+ function validateRunsOn(runsOn, jobName, location) {
482
511
  const { include, exclude } = normalizeRunsOnToMatchers(runsOn, `job '${jobName}' runsOn`);
483
512
  const includeExact = new Set(include.filter((m) => m.kind === "exact").map((m) => m.value));
484
513
  const overlap = exclude.filter((m) => m.kind === "exact").map((m) => m.value).filter((v) => includeExact.has(v));
485
- if (overlap.length > 0) throw new Error(`Job "${jobName}": labels and exclude overlap on [${overlap.join(", ")}]. A label cannot be both required and excluded.`);
514
+ if (overlap.length > 0) throw compilerError("E112", `Job "${jobName}": labels and exclude overlap on [${overlap.join(", ")}]. A label cannot be both required and excluded.`, {
515
+ location,
516
+ suggestion: "Remove the overlapping label(s) from either runsOn or the exclude set."
517
+ });
486
518
  }
487
519
  /**
488
520
  * Transform one context reference (static name or function) into a lock
@@ -490,18 +522,16 @@ function validateRunsOn(runsOn, jobName) {
490
522
  * function becomes an inline expression resolvable at two-phase eval; an impure
491
523
  * one carries only the `dynamic` flag (the agent runs an init job to resolve it).
492
524
  */
493
- function transformContextRef(ref, jobName) {
525
+ function transformContextRef(ref) {
494
526
  if (typeof ref === "function") {
495
527
  const fnSource = ref.toString();
496
- const purity = analyzePurity(fnSource);
497
- if (purity.pure) return {
528
+ if (analyzePurity(fnSource).pure) return {
498
529
  value: {
499
530
  _type: "inline",
500
531
  expression: fnSource
501
532
  },
502
533
  dynamic: true
503
534
  };
504
- console.warn(`[kici] Job "${jobName}": context function is not pure (${purity.reason}). An init job will be required, adding ~5-10s delay.`);
505
535
  return {
506
536
  value: "",
507
537
  dynamic: true
@@ -516,15 +546,25 @@ function transformContextRef(ref, jobName) {
516
546
  * Transform a static job to lock file format.
517
547
  */
518
548
  function transformJob(job, configPath, index, gitRoot, uuidToName) {
519
- if (job.runsOn !== void 0 && job.runsOnAll !== void 0) throw new Error(`job '${job.name}': runsOn and runsOnAll are mutually exclusive`);
520
- if (job.runsOn === void 0 && job.runsOnAll === void 0) throw new Error(`job '${job.name}': one of runsOn or runsOnAll is required`);
549
+ const jobLocation = locationForJob(job, configPath);
550
+ if (job.runsOn !== void 0 && job.runsOnAll !== void 0) throw compilerError("E108", `job '${job.name}': runsOn and runsOnAll are mutually exclusive`, {
551
+ location: jobLocation,
552
+ suggestion: "Set exactly one of runsOn (single agent) or runsOnAll (fan-out to every matching agent)."
553
+ });
554
+ if (job.runsOn === void 0 && job.runsOnAll === void 0) throw compilerError("E109", `job '${job.name}': one of runsOn or runsOnAll is required`, {
555
+ location: jobLocation,
556
+ suggestion: "Add runsOn: \"kici:os:linux\" (or another agent label) to the job."
557
+ });
521
558
  if (job.onUnreachable !== void 0 && job.runsOnAll === void 0) console.warn(`[kici] job '${job.name}': onUnreachable is ignored without runsOnAll`);
522
- if (job.maxParallel !== void 0 && job.maxParallel < 1) throw new Error(`job '${job.name}': maxParallel must be >= 1`);
559
+ if (job.maxParallel !== void 0 && job.maxParallel < 1) throw compilerError("E110", `job '${job.name}': maxParallel must be >= 1`, {
560
+ location: jobLocation,
561
+ suggestion: "Set maxParallel to a positive integer, or remove it to run unbounded."
562
+ });
523
563
  if (!(job.matrix !== void 0 || job.runsOnAll !== void 0) && (job.maxParallel !== void 0 || job.failFast !== void 0)) console.warn(`[kici] job '${job.name}': maxParallel/failFast are ignored without matrix or runsOnAll (no fan-out to bound)`);
524
- if (job.runsOn !== void 0) validateRunsOn(job.runsOn, job.name);
564
+ if (job.runsOn !== void 0) validateRunsOn(job.runsOn, job.name, jobLocation);
525
565
  const contextFields = {};
526
566
  const contextRefs = job.contexts ?? (job.context !== void 0 ? [job.context] : void 0);
527
- if (contextRefs !== void 0 && contextRefs.length > 0) contextFields.contexts = contextRefs.map((ref) => transformContextRef(ref, job.name));
567
+ if (contextRefs !== void 0 && contextRefs.length > 0) contextFields.contexts = contextRefs.map((ref) => transformContextRef(ref));
528
568
  const envFields = {};
529
569
  if (job.env !== void 0) {
530
570
  if (typeof job.env === "function") {
@@ -535,14 +575,16 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
535
575
  _type: "inline",
536
576
  expression: fnSource
537
577
  };
538
- else console.warn(`[kici] Job "${job.name}": env function is not pure (${purity.reason}). An init job will be required, adding ~5-10s delay.`);
539
578
  } else if (typeof job.env === "object") envFields.env = { ...job.env };
540
579
  }
541
580
  if (job.resources !== void 0) try {
542
581
  validateResourceRequest(job.resources);
543
582
  } catch (err) {
544
583
  const reason = err instanceof Error ? err.message : String(err);
545
- throw new Error(`Job "${job.name}": invalid resources -- ${reason}`);
584
+ throw compilerError("E111", `Job "${job.name}": invalid resources -- ${reason}`, {
585
+ location: jobLocation,
586
+ suggestion: "Fix the resources request (valid CPU/memory strings, request <= limit)."
587
+ });
546
588
  }
547
589
  const concurrencyFields = {};
548
590
  if (job.concurrencyGroup !== void 0) {
@@ -554,7 +596,6 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
554
596
  _type: "inline",
555
597
  expression: fnSource
556
598
  };
557
- else console.warn(`[kici] Job "${job.name}": concurrencyGroup function is not pure (${purity.reason}). An init job will be required, adding ~5-10s delay.`);
558
599
  } else if (typeof job.concurrencyGroup === "string") concurrencyFields.concurrencyGroup = job.concurrencyGroup;
559
600
  }
560
601
  return {
@@ -567,7 +608,7 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
567
608
  ...job.maxParallel !== void 0 && { maxParallel: job.maxParallel },
568
609
  ...job.failFast !== void 0 && { failFast: job.failFast },
569
610
  ...resolveNeedsForLock(job.needs, uuidToName),
570
- steps: transformSteps(job.steps, gitRoot),
611
+ steps: transformSteps(job.steps, gitRoot, jobLocation),
571
612
  matrix: job.matrix ? transformMatrix(job.matrix, job.name, configPath) : void 0,
572
613
  include: job.include?.map((inc) => ({ ...inc })),
573
614
  exclude: job.exclude?.map((exc) => ({ ...exc })),
@@ -576,6 +617,7 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
576
617
  ...job.checkout !== void 0 && { checkout: job.checkout },
577
618
  ...job.cache !== void 0 && { cache: normalizeCacheSpecs(job.cache) },
578
619
  ...job.container !== void 0 && { container: job.container },
620
+ ...job.sandbox !== void 0 && { sandbox: job.sandbox },
579
621
  ...contextFields,
580
622
  ...envFields,
581
623
  ...concurrencyFields,
@@ -589,7 +631,7 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
589
631
  ...job.timeout !== void 0 && { timeout: job.timeout },
590
632
  ...job.resources !== void 0 && { resources: job.resources },
591
633
  ...job.init !== void 0 && { init: job.init },
592
- ...job.approval !== void 0 && { approval: (assertNonStepApprovalScope(job.approval, "job"), toLockApproval(job.approval)) }
634
+ ...job.approval !== void 0 && { approval: (assertNonStepApprovalScope(job.approval, "job", jobLocation), toLockApproval(job.approval)) }
593
635
  };
594
636
  }
595
637
  /**
@@ -653,15 +695,21 @@ function toLockApproval(c) {
653
695
  * step-scope-only gate (it fires between a step's check and run), so it is a
654
696
  * compile error anywhere else.
655
697
  */
656
- function assertNonStepApprovalScope(c, scope) {
657
- if (normalizeApproval(c).when === "drift") throw new Error(`approval.when "drift" is only valid on steps (found at ${scope} scope)`);
698
+ function assertNonStepApprovalScope(c, scope, location) {
699
+ if (normalizeApproval(c).when === "drift") throw compilerError("E113", `approval.when "drift" is only valid on steps (found at ${scope} scope)`, {
700
+ location,
701
+ suggestion: "Move the drift-gated approval onto a step, or use when: \"always\" at job/workflow scope."
702
+ });
658
703
  }
659
704
  /**
660
705
  * Validate a step's approval config: `when: 'drift'` fires between the step's
661
706
  * check and run, so it requires a `check` facet. A compile error otherwise.
662
707
  */
663
- function assertStepApprovalCheckFacet(step) {
664
- if (step.approval !== void 0 && normalizeApproval(step.approval).when === "drift" && step.check === void 0) throw new Error(`step '${step.name || "(unnamed)"}': approval.when "drift" requires a check facet`);
708
+ function assertStepApprovalCheckFacet(step, jobLocation) {
709
+ if (step.approval !== void 0 && normalizeApproval(step.approval).when === "drift" && step.check === void 0) throw compilerError("E114", `step '${step.name || "(unnamed)"}': approval.when "drift" requires a check facet`, {
710
+ location: step._sourceLocation ?? jobLocation,
711
+ suggestion: "Add a check facet to the step, or use approval when: \"always\"."
712
+ });
665
713
  }
666
714
  /**
667
715
  * Transform a job's `steps` array into lock-file entries. Sequential steps and
@@ -670,22 +718,40 @@ function assertStepApprovalCheckFacet(step) {
670
718
  * compiler's naming matches the agent's `extractAndNormalizeSteps` enumeration —
671
719
  * the flat-stepIndex invariant.
672
720
  */
673
- function transformSteps(steps, gitRoot) {
721
+ function transformSteps(steps, gitRoot, jobLocation) {
722
+ const anchor = jobLocation ?? {
723
+ file: "",
724
+ line: 1,
725
+ column: 1
726
+ };
674
727
  const counter = { n: 0 };
675
728
  let groupOrdinal = 0;
676
729
  return steps.map((entry) => {
677
- if (isParallelGroup(entry)) return transformParallelGroup(entry, gitRoot, counter, groupOrdinal++);
678
- return transformSequentialStep(entry, gitRoot, counter);
730
+ if (isParallelGroup(entry)) return transformParallelGroup(entry, gitRoot, counter, groupOrdinal++, anchor);
731
+ return transformSequentialStep(entry, gitRoot, counter, anchor);
679
732
  });
680
733
  }
734
+ /** Best step-scoped location: the step's captured call-site, else the job anchor. */
735
+ function stepEntryLocation(entry, jobLocation) {
736
+ return entry._sourceLocation ?? jobLocation;
737
+ }
681
738
  /** Validate and transform a `ParallelGroup` into a `LockParallelStep`. */
682
- function transformParallelGroup(group, gitRoot, counter, groupOrdinal) {
683
- if (group.steps.length === 0) throw new Error("job step: empty parallel group not allowed");
739
+ function transformParallelGroup(group, gitRoot, counter, groupOrdinal, jobLocation) {
740
+ if (group.steps.length === 0) throw compilerError("E115", "job step: empty parallel group not allowed", {
741
+ location: jobLocation,
742
+ suggestion: "Add at least one step to the parallel group, or remove the group."
743
+ });
684
744
  const seen = /* @__PURE__ */ new Set();
685
745
  const children = group.steps.map((child) => {
686
- if (isParallelGroup(child)) throw new Error("job step: nested parallel groups are not supported");
687
- const lockChild = transformSequentialStep(child, gitRoot, counter);
688
- if (seen.has(lockChild.name)) throw new Error(`job step: duplicate step name '${lockChild.name}' in parallel group`);
746
+ if (isParallelGroup(child)) throw compilerError("E116", "job step: nested parallel groups are not supported", {
747
+ location: stepEntryLocation(child, jobLocation),
748
+ suggestion: "Flatten the nested group a parallel group may only contain steps."
749
+ });
750
+ const lockChild = transformSequentialStep(child, gitRoot, counter, jobLocation);
751
+ if (seen.has(lockChild.name)) throw compilerError("E117", `job step: duplicate step name '${lockChild.name}' in parallel group`, {
752
+ location: stepEntryLocation(child, jobLocation),
753
+ suggestion: "Give each step in a parallel group a unique name."
754
+ });
689
755
  seen.add(lockChild.name);
690
756
  return lockChild;
691
757
  });
@@ -699,7 +765,7 @@ function transformParallelGroup(group, gitRoot, counter, groupOrdinal) {
699
765
  };
700
766
  }
701
767
  /** Transform a single sequential step (or bare function) into a `LockStep`. */
702
- function transformSequentialStep(stepOrFn, gitRoot, counter) {
768
+ function transformSequentialStep(stepOrFn, gitRoot, counter, jobLocation) {
703
769
  if (typeof stepOrFn === "function") {
704
770
  counter.n++;
705
771
  return {
@@ -733,7 +799,7 @@ function transformSequentialStep(stepOrFn, gitRoot, counter) {
733
799
  ...step.cleanup !== void 0 && { hasCleanup: true },
734
800
  ...step.check !== void 0 && { hasCheck: true },
735
801
  ...step.whenInSync !== void 0 && { hasWhenInSync: true },
736
- ...step.approval !== void 0 && { approval: (assertStepApprovalCheckFacet(step), toLockApproval(step.approval)) }
802
+ ...step.approval !== void 0 && { approval: (assertStepApprovalCheckFacet(step, jobLocation), toLockApproval(step.approval)) }
737
803
  };
738
804
  }
739
805
  /**
@@ -812,6 +878,6 @@ function serializeLockFile(lockFile, pretty = true) {
812
878
  return JSON.stringify(lockFile, null, pretty ? 2 : void 0);
813
879
  }
814
880
  //#endregion
815
- export { computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile, transformSteps, transformTriggers };
881
+ export { computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile, transformSteps, transformTriggers };
816
882
 
817
883
  //# sourceMappingURL=generator.js.map
@@ -1,3 +1,5 @@
1
- export { generateLockFile, serializeLockFile, detectGitRoot, computeLockfileHash, } from './generator.js';
1
+ export { generateLockFile, serializeLockFile, detectGitRoot, computeLockfileHash, schemaWindowWarning, } from './generator.js';
2
2
  export { computeContentHash, COMPILE_SCHEMA_VERSION } from './hasher.js';
3
+ export { DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings, } from './purity-diagnostics.js';
4
+ export type { JobPurityWarning } from './purity-diagnostics.js';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  import { COMPILE_SCHEMA_VERSION, computeContentHash } from "./hasher.js";
3
- import { computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile } from "./generator.js";
4
- export { COMPILE_SCHEMA_VERSION, computeContentHash, computeLockfileHash, detectGitRoot, generateLockFile, serializeLockFile };
3
+ import { computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile } from "./generator.js";
4
+ import { DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings } from "./purity-diagnostics.js";
5
+ export { COMPILE_SCHEMA_VERSION, DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings, computeContentHash, computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile };
@@ -0,0 +1,31 @@
1
+ import type { Job } from '@kici-dev/sdk';
2
+ import type { WorkflowWithSource } from '../types.js';
3
+ /** Which dynamic-value slot on a job carried the impure function. */
4
+ export declare enum DynamicValueField {
5
+ Context = "context",
6
+ Env = "env",
7
+ ConcurrencyGroup = "concurrencyGroup"
8
+ }
9
+ /**
10
+ * One impure dynamic-value function found on a static job. Each impurity forces
11
+ * the orchestrator to dispatch an agent-side `__init__` job (~5-10s) instead of
12
+ * inlining the value, so we surface it to the author at compile / preview time.
13
+ */
14
+ export interface JobPurityWarning {
15
+ workflowName: string;
16
+ sourceFile?: string;
17
+ jobName: string;
18
+ field: DynamicValueField;
19
+ reason: string;
20
+ }
21
+ /**
22
+ * Analyze a single static job's dynamic-value functions (context(s), env,
23
+ * concurrencyGroup) for impurity. Returns one warning per impure function.
24
+ */
25
+ export declare function analyzeJobPurity(job: Job, workflowName: string, sourceFile?: string): JobPurityWarning[];
26
+ /**
27
+ * Walk every workflow's static jobs and collect impurity warnings. Function-typed
28
+ * jobs (dynamic job generators) are skipped — their purity is analyzed at dispatch.
29
+ */
30
+ export declare function collectWorkflowPurityWarnings(workflows: WorkflowWithSource[]): JobPurityWarning[];
31
+ //# sourceMappingURL=purity-diagnostics.d.ts.map
@@ -0,0 +1,52 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { analyzePurity } from "./purity-analyzer.js";
3
+ //#region src/lockfile/purity-diagnostics.ts
4
+ /** Which dynamic-value slot on a job carried the impure function. */
5
+ let DynamicValueField = /* @__PURE__ */ function(DynamicValueField) {
6
+ DynamicValueField["Context"] = "context";
7
+ DynamicValueField["Env"] = "env";
8
+ DynamicValueField["ConcurrencyGroup"] = "concurrencyGroup";
9
+ return DynamicValueField;
10
+ }({});
11
+ function pushIfImpure(out, fn, field, base) {
12
+ if (typeof fn !== "function") return;
13
+ const result = analyzePurity(fn.toString());
14
+ if (!result.pure) out.push({
15
+ ...base,
16
+ field,
17
+ reason: result.reason ?? "unknown"
18
+ });
19
+ }
20
+ /**
21
+ * Analyze a single static job's dynamic-value functions (context(s), env,
22
+ * concurrencyGroup) for impurity. Returns one warning per impure function.
23
+ */
24
+ function analyzeJobPurity(job, workflowName, sourceFile) {
25
+ const out = [];
26
+ const base = {
27
+ workflowName,
28
+ sourceFile,
29
+ jobName: job.name
30
+ };
31
+ const contextRefs = job.contexts ?? (job.context !== void 0 ? [job.context] : void 0);
32
+ if (contextRefs) for (const ref of contextRefs) pushIfImpure(out, ref, "context", base);
33
+ pushIfImpure(out, job.env, "env", base);
34
+ pushIfImpure(out, job.concurrencyGroup, "concurrencyGroup", base);
35
+ return out;
36
+ }
37
+ /**
38
+ * Walk every workflow's static jobs and collect impurity warnings. Function-typed
39
+ * jobs (dynamic job generators) are skipped — their purity is analyzed at dispatch.
40
+ */
41
+ function collectWorkflowPurityWarnings(workflows) {
42
+ const out = [];
43
+ for (const { workflow, source } of workflows) for (const job of workflow.jobs) {
44
+ if (typeof job === "function") continue;
45
+ out.push(...analyzeJobPurity(job, workflow.name, source.file));
46
+ }
47
+ return out;
48
+ }
49
+ //#endregion
50
+ export { DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings };
51
+
52
+ //# sourceMappingURL=purity-diagnostics.js.map
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { spawn } from "node:child_process";
6
6
  import { confirm } from "@inquirer/prompts";
7
+ import { isCiEnvironment } from "@kici-dev/core/ci-env";
7
8
  //#region src/postinstall.ts
8
9
  /**
9
10
  * Post-install script for @kici-dev/compiler
@@ -33,7 +34,7 @@ async function detectDevelopmentMode() {
33
34
  }
34
35
  async function main() {
35
36
  if (await detectDevelopmentMode()) return;
36
- if (process.env.CI === "true" || !process.stdout.isTTY) return;
37
+ if (isCiEnvironment() || !process.stdout.isTTY) return;
37
38
  const fs = await import("node:fs/promises");
38
39
  try {
39
40
  await fs.access(".kici");
@@ -0,0 +1,8 @@
1
+ /** Sub-directory of the destination that home-anchored entries land under. */
2
+ export declare const HOME_SUBDIR = "~home";
3
+ /**
4
+ * Extract an artifact tarball under `destDir`, relocating the anchor groups so
5
+ * nothing escapes the destination.
6
+ */
7
+ export declare function extractArtifactTarball(tarball: Buffer, destDir: string): Promise<void>;
8
+ //# sourceMappingURL=artifact-extract.d.ts.map
@@ -0,0 +1,58 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { join } from "node:path";
3
+ import { cp, mkdir, readdir } from "node:fs/promises";
4
+ import { HOME_ANCHOR, REPO_ANCHOR } from "@kici-dev/core";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { x } from "tar";
8
+ import { makeTempDir } from "@kici-dev/core/tmp";
9
+ //#region src/remote/artifact-extract.ts
10
+ /**
11
+ * CLI-side artifact extraction.
12
+ *
13
+ * Artifact tarballs are packed by the cache packer, so entries are anchored as
14
+ * `__repo__/<rel>` (repo-root-relative) and `__home__/<rel>` (home-relative).
15
+ * The in-workflow extractor restores those groups to the real repo root / home
16
+ * dir; a CLI must NOT — that would scatter files across the user's filesystem.
17
+ * This helper lands everything under a single destination dir instead:
18
+ *
19
+ * `__repo__/<rel>` -> `<destDir>/<rel>`
20
+ * `__home__/<rel>` -> `<destDir>/~home/<rel>` (kept separate so a
21
+ * home-anchored file cannot clobber a repo-anchored one)
22
+ *
23
+ * Extraction goes through a scratch dir first and only relocates on success, so
24
+ * a failed extraction never leaves a half-written output directory.
25
+ */
26
+ /** Sub-directory of the destination that home-anchored entries land under. */
27
+ const HOME_SUBDIR = "~home";
28
+ /**
29
+ * Extract an artifact tarball under `destDir`, relocating the anchor groups so
30
+ * nothing escapes the destination.
31
+ */
32
+ async function extractArtifactTarball(tarball, destDir) {
33
+ const scratch = await makeTempDir("artifact-extract");
34
+ try {
35
+ await pipeline(Readable.from(tarball), x({
36
+ cwd: scratch.path,
37
+ gzip: true
38
+ }));
39
+ await mkdir(destDir, { recursive: true });
40
+ for (const top of await readdir(scratch.path)) {
41
+ const from = join(scratch.path, top);
42
+ if (top === REPO_ANCHOR) await relocateGroup(from, destDir);
43
+ else if (top === HOME_ANCHOR) await relocateGroup(from, join(destDir, HOME_SUBDIR));
44
+ else await cp(from, join(destDir, top), { recursive: true });
45
+ }
46
+ } finally {
47
+ await scratch.cleanup();
48
+ }
49
+ }
50
+ /** Copy the contents of an anchor group dir into `into`, merging trees. */
51
+ async function relocateGroup(groupDir, into) {
52
+ await mkdir(into, { recursive: true });
53
+ for (const entry of await readdir(groupDir)) await cp(join(groupDir, entry), join(into, entry), { recursive: true });
54
+ }
55
+ //#endregion
56
+ export { HOME_SUBDIR, extractArtifactTarball };
57
+
58
+ //# sourceMappingURL=artifact-extract.js.map
@@ -32,13 +32,31 @@ export interface GlobalConfig {
32
32
  }
33
33
  /**
34
34
  * Returns the global KiCI config directory path.
35
- * Checks KICI_CONFIG_DIR env var first, falls back to ~/.kici.
35
+ *
36
+ * Resolution order:
37
+ * 1. `KICI_CONFIG_DIR`, when set to a non-empty value.
38
+ * 2. A hard refusal when `KICI_TEST_ISOLATION` is present. A developer
39
+ * machine's `~/.kici/config` names a real endpoint and carries a live
40
+ * PAT, so a test process that reads it authenticates against whatever
41
+ * that config points at — in practice, production. Every test that needs
42
+ * a config must name its own isolated directory.
43
+ * 3. `~/.kici`.
44
+ *
45
+ * `KICI_TEST_ISOLATION` is set by this repository's own vitest configs, via
46
+ * `hack/lib/vitest-isolation.ts`. It is deliberately a KiCI-owned name rather
47
+ * than the runner's `VITEST`: `kici` is a compat-protected CLI, and `VITEST`
48
+ * is a third-party marker that propagates into spawned children, so keying off
49
+ * it would break a customer whose vitest test shells out to `kici`.
50
+ *
51
+ * `env` is injectable so this module's own tests can exercise every branch.
52
+ * It is NOT an opt-out: production call sites pass no argument and inherit the
53
+ * guard.
36
54
  */
37
- export declare function getConfigDir(): string;
55
+ export declare function getConfigDir(env?: NodeJS.ProcessEnv): string;
38
56
  /**
39
57
  * Returns the path to the global config file (~/.kici/config).
40
58
  */
41
- export declare function getConfigPath(): string;
59
+ export declare function getConfigPath(env?: NodeJS.ProcessEnv): string;
42
60
  /**
43
61
  * Load the global KiCI config from ~/.kici/config.
44
62
  * Returns an empty object if the file does not exist.
@@ -30,17 +30,36 @@ function sanitizeConfig(raw) {
30
30
  }
31
31
  /**
32
32
  * Returns the global KiCI config directory path.
33
- * Checks KICI_CONFIG_DIR env var first, falls back to ~/.kici.
33
+ *
34
+ * Resolution order:
35
+ * 1. `KICI_CONFIG_DIR`, when set to a non-empty value.
36
+ * 2. A hard refusal when `KICI_TEST_ISOLATION` is present. A developer
37
+ * machine's `~/.kici/config` names a real endpoint and carries a live
38
+ * PAT, so a test process that reads it authenticates against whatever
39
+ * that config points at — in practice, production. Every test that needs
40
+ * a config must name its own isolated directory.
41
+ * 3. `~/.kici`.
42
+ *
43
+ * `KICI_TEST_ISOLATION` is set by this repository's own vitest configs, via
44
+ * `hack/lib/vitest-isolation.ts`. It is deliberately a KiCI-owned name rather
45
+ * than the runner's `VITEST`: `kici` is a compat-protected CLI, and `VITEST`
46
+ * is a third-party marker that propagates into spawned children, so keying off
47
+ * it would break a customer whose vitest test shells out to `kici`.
48
+ *
49
+ * `env` is injectable so this module's own tests can exercise every branch.
50
+ * It is NOT an opt-out: production call sites pass no argument and inherit the
51
+ * guard.
34
52
  */
35
- function getConfigDir() {
36
- if (process.env.KICI_CONFIG_DIR) return process.env.KICI_CONFIG_DIR;
53
+ function getConfigDir(env = process.env) {
54
+ if (env.KICI_CONFIG_DIR) return env.KICI_CONFIG_DIR;
55
+ if (env.KICI_TEST_ISOLATION) throw new Error("Refusing to read the ambient ~/.kici config from a test process. Set KICI_CONFIG_DIR to an isolated directory for this run — the developer machine config may point at production.");
37
56
  return path.join(os.homedir(), ".kici");
38
57
  }
39
58
  /**
40
59
  * Returns the path to the global config file (~/.kici/config).
41
60
  */
42
- function getConfigPath() {
43
- return path.join(getConfigDir(), "config");
61
+ function getConfigPath(env = process.env) {
62
+ return path.join(getConfigDir(env), "config");
44
63
  }
45
64
  /**
46
65
  * Load the global KiCI config from ~/.kici/config.