@kici-dev/compiler 0.1.27 → 0.2.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 +48 -19
  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 +6124 -4423
  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
@@ -0,0 +1,227 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import fs from "node:fs";
3
+ import { execFile } from "node:child_process";
4
+ import net from "node:net";
5
+ //#region src/local-plane/port-holder.ts
6
+ /**
7
+ * OS-level ownership of the local dev plane's port.
8
+ *
9
+ * Answers two questions the plane's on-disk stamp cannot: is the port occupied
10
+ * at all, and which process occupies it. Every external command is reached
11
+ * through an injectable `ExecFn` so each platform branch has a real unit test on
12
+ * any host, and every failure degrades to `null` rather than throwing — a
13
+ * discovery failure must never break the command that asked.
14
+ */
15
+ const defaultExec = (cmd, args) => new Promise((resolve) => {
16
+ execFile(cmd, args, {
17
+ timeout: 5e3,
18
+ maxBuffer: 8 * 1024 * 1024
19
+ }, (err, stdout) => {
20
+ const raw = err?.code;
21
+ resolve({
22
+ stdout: stdout ?? "",
23
+ exitCode: typeof raw === "number" ? raw : err ? 1 : 0
24
+ });
25
+ });
26
+ });
27
+ /**
28
+ * Command line of process `pid`, or null when it cannot be determined.
29
+ *
30
+ * Linux exposes it as a NUL-separated `/proc` file; every other platform is
31
+ * asked through the same injectable `ExecFn` the port discovery above uses.
32
+ *
33
+ * **A null means "cannot tell", never "not that process."** Callers must treat
34
+ * it as an absence of evidence — the only safe reading when the answer decides
35
+ * whether to signal a pid.
36
+ */
37
+ async function processCommandLine(pid, opts = {}) {
38
+ const platform = opts.platform ?? process.platform;
39
+ try {
40
+ if (platform === "linux") {
41
+ const joined = (opts.readProc ?? ((p) => fs.readFileSync(p, "utf-8")))(`/proc/${pid}/cmdline`).split("\0").filter(Boolean).join(" ").trim();
42
+ return joined === "" ? null : joined;
43
+ }
44
+ if (platform === "win32") return null;
45
+ const r = await (opts.exec ?? defaultExec)("ps", [
46
+ "-o",
47
+ "args=",
48
+ "-p",
49
+ String(pid)
50
+ ]);
51
+ if (r.exitCode !== 0) return null;
52
+ const line = r.stdout.trim();
53
+ return line === "" ? null : line;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ /**
59
+ * Whether the port can be bound on loopback. Probes `127.0.0.1` because that is
60
+ * the address the CLI talks to; a listener bound to `0.0.0.0` still makes this
61
+ * bind fail with EADDRINUSE, so there is no false "free".
62
+ */
63
+ function isPortFree(port) {
64
+ return new Promise((resolve) => {
65
+ const probe = net.createServer();
66
+ probe.once("error", () => resolve(false));
67
+ probe.listen({
68
+ port,
69
+ host: "127.0.0.1",
70
+ exclusive: true
71
+ }, () => {
72
+ probe.close(() => resolve(true));
73
+ });
74
+ });
75
+ }
76
+ /**
77
+ * Port of a socket address column. The port is the segment after the LAST
78
+ * colon, because an IPv6 address is written `[::]:5355`.
79
+ */
80
+ function addressPort(addr) {
81
+ const i = addr.lastIndexOf(":");
82
+ if (i < 0) return null;
83
+ const n = Number(addr.slice(i + 1));
84
+ return Number.isInteger(n) ? n : null;
85
+ }
86
+ /**
87
+ * Parse `ss -lptnH` rows. The process name is deliberately NOT matched — a Node
88
+ * listener reports as `MainThread` — so only `pid=N` is extracted. A row for a
89
+ * socket this user does not own carries no `users:(…)` field at all and yields
90
+ * null, which is what sends the caller to the lsof fallback.
91
+ */
92
+ function parseSsOutput(stdout, port) {
93
+ for (const line of stdout.split("\n")) {
94
+ const cols = line.trim().split(/\s+/);
95
+ if (cols.length < 4) continue;
96
+ if (addressPort(cols[3]) !== port) continue;
97
+ const m = /pid=(\d+)/.exec(line);
98
+ return m ? Number(m[1]) : null;
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Parse `lsof -Fpn` field output, which is a stateful stream: each `p<pid>` line
104
+ * precedes the `n<address>` lines belonging to that pid.
105
+ */
106
+ function parseLsofOutput(stdout, port) {
107
+ let current = null;
108
+ for (const line of stdout.split("\n")) if (line.startsWith("p")) {
109
+ const n = Number(line.slice(1));
110
+ current = Number.isInteger(n) ? n : null;
111
+ } else if (line.startsWith("n") && current !== null) {
112
+ if (addressPort(line.slice(1)) === port) return current;
113
+ }
114
+ return null;
115
+ }
116
+ /** Parse `netstat -ano -p tcp`: LISTENING rows only, pid in the last column. */
117
+ function parseNetstatOutput(stdout, port) {
118
+ for (const line of stdout.split("\n")) {
119
+ const cols = line.trim().split(/\s+/);
120
+ if (cols.length < 5) continue;
121
+ if (!/^LISTENING$/i.test(cols[3])) continue;
122
+ if (addressPort(cols[1]) !== port) continue;
123
+ const pid = Number(cols[cols.length - 1]);
124
+ return Number.isInteger(pid) ? pid : null;
125
+ }
126
+ return null;
127
+ }
128
+ /**
129
+ * Pid of the process listening on `port`, or null when it cannot be determined.
130
+ * Each platform runs the UNFILTERED listing and matches the port here rather
131
+ * than passing a filter expression to the tool, whose syntax is the fragile
132
+ * part. On Linux a null parse falls through to lsof as well as a non-zero exit,
133
+ * because ss omits the owning-process field for sockets this user does not own.
134
+ */
135
+ async function findPortHolderPid(port, opts = {}) {
136
+ const exec = opts.exec ?? defaultExec;
137
+ const platform = opts.platform ?? process.platform;
138
+ const viaLsof = async () => {
139
+ const r = await exec("lsof", [
140
+ "-nP",
141
+ "-iTCP",
142
+ "-sTCP:LISTEN",
143
+ "-Fpn"
144
+ ]);
145
+ return r.exitCode === 0 ? parseLsofOutput(r.stdout, port) : null;
146
+ };
147
+ try {
148
+ if (platform === "win32") {
149
+ const r = await exec("netstat", [
150
+ "-ano",
151
+ "-p",
152
+ "tcp"
153
+ ]);
154
+ return r.exitCode === 0 ? parseNetstatOutput(r.stdout, port) : null;
155
+ }
156
+ if (platform === "linux") {
157
+ const r = await exec("ss", ["-lptnH"]);
158
+ return (r.exitCode === 0 ? parseSsOutput(r.stdout, port) : null) ?? await viaLsof();
159
+ }
160
+ return await viaLsof();
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+ const defaultKill = (pid, signal) => process.kill(pid, signal);
166
+ /** How long to keep watching for the process to disappear after SIGKILL. */
167
+ const HARD_KILL_WAIT_MS = 5e3;
168
+ /**
169
+ * Poll until process `pid` has exited, sending SIGTERM first and escalating to
170
+ * SIGKILL past the grace window. `process.kill(pid, 0)` throws ESRCH once the
171
+ * process is gone. On Windows every signal terminates unconditionally, so the
172
+ * escalation collapses into a single hard kill there.
173
+ *
174
+ * Resolving does NOT prove the process died: a pid that survives SIGKILL (a
175
+ * zombie awaiting reaping, or a task wedged in uninterruptible I/O) is given
176
+ * `hardKillWaitMs` and then abandoned, because a teardown command that hangs
177
+ * forever is worse than one that reports the port is still held. Callers decide
178
+ * the outcome from the port itself — `waitForPortFree` — never from this
179
+ * resolving.
180
+ */
181
+ async function terminatePid(pid, graceMs = 1e4, opts = {}) {
182
+ const kill = opts.kill ?? defaultKill;
183
+ const hardKillWaitMs = opts.hardKillWaitMs ?? HARD_KILL_WAIT_MS;
184
+ try {
185
+ kill(pid, "SIGTERM");
186
+ } catch {
187
+ return;
188
+ }
189
+ let deadline = Date.now() + graceMs;
190
+ let killed = false;
191
+ for (;;) {
192
+ try {
193
+ kill(pid, 0);
194
+ } catch {
195
+ return;
196
+ }
197
+ if (Date.now() > deadline) {
198
+ if (killed) return;
199
+ try {
200
+ kill(pid, "SIGKILL");
201
+ } catch {
202
+ return;
203
+ }
204
+ killed = true;
205
+ deadline = Date.now() + hardKillWaitMs;
206
+ }
207
+ await new Promise((r) => setTimeout(r, 100));
208
+ }
209
+ }
210
+ /**
211
+ * Poll until the port is bindable. This, not `terminatePid` resolving, is what
212
+ * decides a teardown: `terminatePid` abandons a pid that survives SIGKILL, and
213
+ * even a process that did exit can leave its listening socket lingering a moment
214
+ * longer on some platforms.
215
+ */
216
+ async function waitForPortFree(port, timeoutMs = 5e3) {
217
+ const deadline = Date.now() + timeoutMs;
218
+ for (;;) {
219
+ if (await isPortFree(port)) return true;
220
+ if (Date.now() >= deadline) return false;
221
+ await new Promise((r) => setTimeout(r, 100));
222
+ }
223
+ }
224
+ //#endregion
225
+ export { findPortHolderPid, isPortFree, parseLsofOutput, parseNetstatOutput, parseSsOutput, processCommandLine, terminatePid, waitForPortFree };
226
+
227
+ //# sourceMappingURL=port-holder.js.map
@@ -18,12 +18,24 @@ declare let embeddedDaemon: typeof defaultEmbeddedDaemon;
18
18
  /** Test seam: override the embedded postmaster daemonizer. */
19
19
  export declare function __setEmbeddedDaemonForTest(fn: typeof embeddedDaemon): void;
20
20
  declare function defaultReadyPoller(_port: number): Promise<boolean>;
21
- /** Whether a podman binary is available on PATH. */
22
- export declare function isPodmanAvailable(): Promise<boolean>;
21
+ /**
22
+ * Whether this plane's embedded postmaster is already serving `port`.
23
+ *
24
+ * `pg_ctl start` fails outright against a running cluster, so a boot that
25
+ * reclaims the plane port while leaving PostgreSQL up — reclaiming another
26
+ * plane's orchestrator does exactly that, since only the port holder is
27
+ * signalled — would otherwise fall through to the Podman path and fail there.
28
+ *
29
+ * The port comes from the cluster's own `postmaster.pid` (line 4) rather than
30
+ * being assumed, so a postmaster left on a different port is not mistaken for
31
+ * one this plane can reuse.
32
+ */
33
+ export declare function embeddedClusterIsServing(port: number): Promise<boolean>;
23
34
  /**
24
35
  * Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
25
36
  * this CLI process (embedded-postgres's in-process server is killed by its own
26
- * exit hook, so it cannot back a warm plane).
37
+ * exit hook, so it cannot back a warm plane). A cluster already serving this
38
+ * plane's port is reused as-is.
27
39
  */
28
40
  declare function defaultEmbeddedDaemon(port: number): Promise<void>;
29
41
  /**
@@ -3,9 +3,9 @@ import { planePaths, planePorts } from "./paths.js";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import fs from "node:fs";
6
+ import { $ } from "zx";
6
7
  import { spawn } from "node:child_process";
7
8
  import os from "node:os";
8
- import { $ } from "zx";
9
9
  import EmbeddedPostgres from "embedded-postgres";
10
10
  //#region src/local-plane/postgres.ts
11
11
  /**
@@ -35,15 +35,6 @@ async function defaultReadyPoller(_port) {
35
35
  }
36
36
  return false;
37
37
  }
38
- /** Whether a podman binary is available on PATH. */
39
- async function isPodmanAvailable() {
40
- try {
41
- await $`podman --version`.quiet();
42
- return true;
43
- } catch {
44
- return false;
45
- }
46
- }
47
38
  /**
48
39
  * Resolve the platform-specific `pg_ctl` binary bundled with embedded-postgres.
49
40
  * Mirrors the package's own platform → binary-package mapping, resolving the
@@ -91,12 +82,43 @@ async function ensureEmbeddedCluster(port) {
91
82
  await pg.stop();
92
83
  }
93
84
  /**
85
+ * Whether this plane's embedded postmaster is already serving `port`.
86
+ *
87
+ * `pg_ctl start` fails outright against a running cluster, so a boot that
88
+ * reclaims the plane port while leaving PostgreSQL up — reclaiming another
89
+ * plane's orchestrator does exactly that, since only the port holder is
90
+ * signalled — would otherwise fall through to the Podman path and fail there.
91
+ *
92
+ * The port comes from the cluster's own `postmaster.pid` (line 4) rather than
93
+ * being assumed, so a postmaster left on a different port is not mistaken for
94
+ * one this plane can reuse.
95
+ */
96
+ async function embeddedClusterIsServing(port) {
97
+ const { pgData } = planePaths();
98
+ const pidFile = path.join(pgData, "postmaster.pid");
99
+ let runningPort;
100
+ try {
101
+ runningPort = Number(fs.readFileSync(pidFile, "utf-8").split("\n")[3]);
102
+ } catch {
103
+ return false;
104
+ }
105
+ if (runningPort !== port) return false;
106
+ try {
107
+ await $`${resolvePgCtl()} -D ${pgData} status`.quiet();
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ /**
94
114
  * Start a detached embedded postmaster via `pg_ctl` so it survives the exit of
95
115
  * this CLI process (embedded-postgres's in-process server is killed by its own
96
- * exit hook, so it cannot back a warm plane).
116
+ * exit hook, so it cannot back a warm plane). A cluster already serving this
117
+ * plane's port is reused as-is.
97
118
  */
98
119
  async function defaultEmbeddedDaemon(port) {
99
120
  const { pgData, logFile } = planePaths();
121
+ if (await embeddedClusterIsServing(port)) return;
100
122
  await $`${resolvePgCtl()} -D ${pgData} -o ${`-p ${port}`} -l ${`${logFile}.pg`} -w start`.quiet();
101
123
  }
102
124
  /** Stop the detached embedded postmaster (handle-independent, reads the data dir). */
@@ -160,6 +182,6 @@ async function startPlanePostgres(opts = {}) {
160
182
  };
161
183
  }
162
184
  //#endregion
163
- export { PLANE_PG_CONTAINER, PLANE_PG_IMAGE, __setEmbeddedDaemonForTest, __setReadyPollerForTest, isPodmanAvailable, startPlanePostgres, stopPlanePostgres };
185
+ export { PLANE_PG_CONTAINER, PLANE_PG_IMAGE, __setEmbeddedDaemonForTest, __setReadyPollerForTest, embeddedClusterIsServing, startPlanePostgres, stopPlanePostgres };
164
186
 
165
187
  //# sourceMappingURL=postgres.js.map
@@ -3,6 +3,16 @@
3
3
  * orchestrator admin API (no SSE in independent mode). Streams step log lines as
4
4
  * they arrive and resolves when the run reaches a terminal state.
5
5
  */
6
+ /**
7
+ * Resolve the acceptance window, falling back to the default for any value that
8
+ * is not a positive number of milliseconds.
9
+ *
10
+ * A malformed override must never reach the deadline arithmetic: `Number('')`
11
+ * is `0` (every run fails instantly) and `Number('2 min')` is `NaN`, which makes
12
+ * `now > deadline` permanently false and silently restores the 900 s hang this
13
+ * window exists to prevent. Both failure modes are worse than ignoring a typo.
14
+ */
15
+ export declare function resolveAcceptanceTimeoutMs(explicitMs: number | undefined, raw: string | undefined): number;
6
16
  /** A minimal admin-read client (AdminApiClient.get) — injectable for tests. */
7
17
  export interface RunFollowClient {
8
18
  get<T>(path: string): Promise<T>;
@@ -35,6 +45,16 @@ export interface FollowRunOptions {
35
45
  timeoutMs?: number;
36
46
  /** Absolute cap regardless of progress (safety net against a runaway follow). Default 2 h. */
37
47
  maxTotalMs?: number;
48
+ /**
49
+ * Max time for the FIRST job to leave `pending`/`queued` — that is, for some
50
+ * agent to claim the run. A plane whose scaler matches no label set, or whose
51
+ * agent cannot start, never gets past this, so it fails in seconds instead of
52
+ * consuming the whole idle window. Default 2 min; override with
53
+ * `KICI_LOCAL_ACCEPTANCE_TIMEOUT_MS`.
54
+ */
55
+ acceptanceTimeoutMs?: number;
56
+ /** Plane log path named in the acceptance-failure message. */
57
+ hintLogPath?: string;
38
58
  }
39
59
  /** Poll a run to completion, streaming step logs. */
40
60
  export declare function followRun(planeUrl: string, adminToken: string, runId: string, opts?: FollowRunOptions): Promise<RunOutcome>;
@@ -1,5 +1,5 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
- import { ExecutionJobStatus, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES } from "@kici-dev/engine";
2
+ import { ExecutionJobStatus, ExecutionRunStatus, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES } from "@kici-dev/engine";
3
3
  import { AdminApiClient } from "@kici-dev/orchestrator";
4
4
  //#region src/local-plane/run-follow.ts
5
5
  /**
@@ -7,29 +7,76 @@ import { AdminApiClient } from "@kici-dev/orchestrator";
7
7
  * orchestrator admin API (no SSE in independent mode). Streams step log lines as
8
8
  * they arrive and resolves when the run reaches a terminal state.
9
9
  */
10
- /** Job states that mean the job did NOT succeed (failed / stale / cancelled / dropped). */
10
+ /**
11
+ * Job states that mean the job did NOT succeed (failed / stale / cancelled /
12
+ * dropped / unroutable). Read only by the anti-false-green guard below, so a
13
+ * missing member lets a lagging `success` run header be reported verbatim even
14
+ * though a job settled badly.
15
+ */
11
16
  const FAILED_JOB_STATES = /* @__PURE__ */ new Set([
12
17
  ExecutionJobStatus.enum.failed,
13
18
  ExecutionJobStatus.enum.timed_out_stale,
14
19
  ExecutionJobStatus.enum.cancelled,
15
- ExecutionJobStatus.enum.drift_dropped
20
+ ExecutionJobStatus.enum.drift_dropped,
21
+ ExecutionJobStatus.enum.unroutable
16
22
  ]);
23
+ /** Job states in which no agent has claimed the job yet. */
24
+ const UNCLAIMED_JOB_STATES = /* @__PURE__ */ new Set([ExecutionJobStatus.enum.pending, ExecutionJobStatus.enum.queued]);
25
+ /**
26
+ * How often the acceptance check re-fetches jobs. Deliberately far coarser than
27
+ * the poll interval: at 750 ms a two-minute window would cost ~160 extra admin
28
+ * requests to answer a question that changes once.
29
+ */
30
+ const ACCEPTANCE_CHECK_INTERVAL_MS = 5e3;
31
+ /** Default window for an agent to claim the run before the follow gives up. */
32
+ const DEFAULT_ACCEPTANCE_TIMEOUT_MS = 12e4;
33
+ /**
34
+ * Resolve the acceptance window, falling back to the default for any value that
35
+ * is not a positive number of milliseconds.
36
+ *
37
+ * A malformed override must never reach the deadline arithmetic: `Number('')`
38
+ * is `0` (every run fails instantly) and `Number('2 min')` is `NaN`, which makes
39
+ * `now > deadline` permanently false and silently restores the 900 s hang this
40
+ * window exists to prevent. Both failure modes are worse than ignoring a typo.
41
+ */
42
+ function resolveAcceptanceTimeoutMs(explicitMs, raw) {
43
+ if (explicitMs !== void 0) return explicitMs;
44
+ if (raw === void 0) return DEFAULT_ACCEPTANCE_TIMEOUT_MS;
45
+ const parsed = Number(raw);
46
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ACCEPTANCE_TIMEOUT_MS;
47
+ }
17
48
  /** Poll a run to completion, streaming step logs. */
18
49
  async function followRun(planeUrl, adminToken, runId, opts = {}) {
19
50
  const client = opts.client ?? new AdminApiClient(planeUrl, adminToken);
20
51
  const pollIntervalMs = opts.pollIntervalMs ?? 750;
21
52
  const idleTimeoutMs = opts.idleTimeoutMs ?? opts.timeoutMs ?? 9e5;
22
53
  const maxTotalMs = opts.maxTotalMs ?? 72e5;
54
+ const acceptanceTimeoutMs = resolveAcceptanceTimeoutMs(opts.acceptanceTimeoutMs, process.env.KICI_LOCAL_ACCEPTANCE_TIMEOUT_MS);
23
55
  const cursors = /* @__PURE__ */ new Map();
24
56
  const stream = !opts.quiet && opts.onLine ? opts.onLine : void 0;
25
57
  const startedAt = Date.now();
26
58
  let lastProgressAt = startedAt;
27
59
  let lastStatus = "";
60
+ let accepted = false;
61
+ let acceptanceDeadline = startedAt + acceptanceTimeoutMs;
62
+ let lastAcceptanceCheck = 0;
28
63
  for (;;) {
29
64
  const now = Date.now();
30
65
  if (now - lastProgressAt > idleTimeoutMs) throw new Error(`offline run: follow timed out (no progress for ${idleTimeoutMs}ms) for run ${runId}`);
31
66
  if (now - startedAt > maxTotalMs) throw new Error(`offline run: follow exceeded its max window (${maxTotalMs}ms) for run ${runId}`);
32
67
  const { run } = await client.get(`/api/v1/admin/runs/${runId}`);
68
+ if (!accepted && !TERMINAL_RUN_STATES.has(run.status)) {
69
+ if (run.status === ExecutionRunStatus.enum.held) acceptanceDeadline = now + acceptanceTimeoutMs;
70
+ else if (now - lastAcceptanceCheck >= ACCEPTANCE_CHECK_INTERVAL_MS) {
71
+ lastAcceptanceCheck = now;
72
+ const claimedJobs = await fetchJobs(client, runId);
73
+ accepted = claimedJobs.length > 0 && claimedJobs.some((j) => !UNCLAIMED_JOB_STATES.has(j.status));
74
+ }
75
+ if (!accepted && now > acceptanceDeadline) {
76
+ const seconds = Math.round(acceptanceTimeoutMs / 1e3);
77
+ throw new Error(`offline run: no agent picked up this run within ${seconds}s — no scaler label set matched the job's runsOn, or the agent failed to start.` + (opts.hintLogPath ? ` Plane log: ${opts.hintLogPath}` : ""));
78
+ }
79
+ }
33
80
  let progressed = false;
34
81
  if (run.status !== lastStatus) {
35
82
  lastStatus = run.status;
@@ -99,6 +146,6 @@ function sleep(ms) {
99
146
  return new Promise((r) => setTimeout(r, ms));
100
147
  }
101
148
  //#endregion
102
- export { followRun };
149
+ export { followRun, resolveAcceptanceTimeoutMs };
103
150
 
104
151
  //# sourceMappingURL=run-follow.js.map
@@ -34,21 +34,35 @@ export declare const TRUSTED_ROUTING_LABEL = "self-hosted";
34
34
  */
35
35
  export declare const IN_PLACE_ROUTING_LABEL = "in-place";
36
36
  /**
37
- * Write the plane's bare-metal scaler YAML and return its path. One scaler with
38
- * two coexisting label sets spawning the agent wrapper against the plane
39
- * orchestrator's localhost WS endpoint:
37
+ * Write the plane's bare-metal scaler YAML and return its path. TWO coexisting
38
+ * bare-metal scalers spawn the agent wrapper against the plane orchestrator's
39
+ * localhost WS endpoint — a **sandboxed** pool and a `self-hosted`-**tainted**
40
+ * TRUSTED pool:
40
41
  *
41
- * - `default` — the sandboxed / credential-isolated profile a normal
42
- * `kici run --local` dispatch (jobs with `runsOn: ['default']` or none) lands
43
- * on. Smallest-set-wins routing keeps default runs here.
44
- * - `default` + `self-hosted` the TRUSTED fleet profile (`KICI_TRUSTED_ENV=true`,
45
- * bwrap off) a `kici run --local --trusted` run (or a `runsOn: self-hosted`
46
- * workflow) lands on: steps run with the ambient host env passed through,
47
- * minus the agent's own KiCI identity secrets.
48
- * - `default` + `self-hosted` + `in-place` the TRUSTED **in-place** profile
49
- * (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted --in-place` run
50
- * lands on: same trusted env, but the agent uses the operator's real working
51
- * tree directly (no clone) — the profile KiCI's own routed `deploy:stg` uses.
42
+ * - `kici-local-bare-metal` (sandboxed) one `default` label set, no taint. The
43
+ * credential-isolated profile a normal `kici run --local` dispatch (jobs with
44
+ * `runsOn: ['default']` or none) lands on. Its agents inherit no
45
+ * `mandatoryLabels`, so a bare `default` job routes here.
46
+ * - `kici-local-bare-metal-trusted` (TRUSTED) carries `mandatoryLabels:
47
+ * ['self-hosted']`, so ONLY a job that explicitly requests `self-hosted`
48
+ * (i.e. a `kici run --local --trusted` run, whose lock patch appends the
49
+ * `self-hosted` routing label, or a `runsOn: self-hosted` workflow) is ever
50
+ * routed to it at BOTH the scaler spawn matcher AND the orchestrator's
51
+ * agent-registry dispatch gate. Two label sets:
52
+ * - `default` + `self-hosted` — the trusted fleet profile
53
+ * (`KICI_TRUSTED_ENV=true`, bwrap off): steps run with the ambient host
54
+ * env passed through, minus the agent's own KiCI identity secrets.
55
+ * - `default` + `self-hosted` + `in-place` — the trusted **in-place**
56
+ * profile (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted
57
+ * --in-place` run lands on: same trusted env, but the agent uses the
58
+ * operator's real working tree directly (no clone) — the profile KiCI's
59
+ * own routed `deploy:stg` uses.
60
+ *
61
+ * The `self-hosted` taint is the isolation guarantee: without it, an idle
62
+ * trusted agent (spawned for an earlier `--trusted` run and lingering for
63
+ * follow-up jobs) would satisfy the agent-registry's subset match for a plain
64
+ * `default` job and leak the ambient host env into a sandboxed run. The taint
65
+ * closes that at the dispatch layer, not just the spawn heuristic.
52
66
  */
53
67
  export declare function writeScalerConfig(orchestratorPort: number): string;
54
68
  //# sourceMappingURL=scaler-config.d.ts.map
@@ -49,60 +49,78 @@ const TRUSTED_ROUTING_LABEL = "self-hosted";
49
49
  */
50
50
  const IN_PLACE_ROUTING_LABEL = "in-place";
51
51
  /**
52
- * Write the plane's bare-metal scaler YAML and return its path. One scaler with
53
- * two coexisting label sets spawning the agent wrapper against the plane
54
- * orchestrator's localhost WS endpoint:
52
+ * Write the plane's bare-metal scaler YAML and return its path. TWO coexisting
53
+ * bare-metal scalers spawn the agent wrapper against the plane orchestrator's
54
+ * localhost WS endpoint — a **sandboxed** pool and a `self-hosted`-**tainted**
55
+ * TRUSTED pool:
55
56
  *
56
- * - `default` — the sandboxed / credential-isolated profile a normal
57
- * `kici run --local` dispatch (jobs with `runsOn: ['default']` or none) lands
58
- * on. Smallest-set-wins routing keeps default runs here.
59
- * - `default` + `self-hosted` the TRUSTED fleet profile (`KICI_TRUSTED_ENV=true`,
60
- * bwrap off) a `kici run --local --trusted` run (or a `runsOn: self-hosted`
61
- * workflow) lands on: steps run with the ambient host env passed through,
62
- * minus the agent's own KiCI identity secrets.
63
- * - `default` + `self-hosted` + `in-place` the TRUSTED **in-place** profile
64
- * (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted --in-place` run
65
- * lands on: same trusted env, but the agent uses the operator's real working
66
- * tree directly (no clone) — the profile KiCI's own routed `deploy:stg` uses.
57
+ * - `kici-local-bare-metal` (sandboxed) one `default` label set, no taint. The
58
+ * credential-isolated profile a normal `kici run --local` dispatch (jobs with
59
+ * `runsOn: ['default']` or none) lands on. Its agents inherit no
60
+ * `mandatoryLabels`, so a bare `default` job routes here.
61
+ * - `kici-local-bare-metal-trusted` (TRUSTED) carries `mandatoryLabels:
62
+ * ['self-hosted']`, so ONLY a job that explicitly requests `self-hosted`
63
+ * (i.e. a `kici run --local --trusted` run, whose lock patch appends the
64
+ * `self-hosted` routing label, or a `runsOn: self-hosted` workflow) is ever
65
+ * routed to it at BOTH the scaler spawn matcher AND the orchestrator's
66
+ * agent-registry dispatch gate. Two label sets:
67
+ * - `default` + `self-hosted` — the trusted fleet profile
68
+ * (`KICI_TRUSTED_ENV=true`, bwrap off): steps run with the ambient host
69
+ * env passed through, minus the agent's own KiCI identity secrets.
70
+ * - `default` + `self-hosted` + `in-place` — the trusted **in-place**
71
+ * profile (adds `KICI_IN_PLACE=true`) a `kici run --local --trusted
72
+ * --in-place` run lands on: same trusted env, but the agent uses the
73
+ * operator's real working tree directly (no clone) — the profile KiCI's
74
+ * own routed `deploy:stg` uses.
75
+ *
76
+ * The `self-hosted` taint is the isolation guarantee: without it, an idle
77
+ * trusted agent (spawned for an earlier `--trusted` run and lingering for
78
+ * follow-up jobs) would satisfy the agent-registry's subset match for a plain
79
+ * `default` job and leak the ambient host env into a sandboxed run. The taint
80
+ * closes that at the dispatch layer, not just the spawn heuristic.
67
81
  */
68
82
  function writeScalerConfig(orchestratorPort) {
69
83
  const { scalerConfigFile, root } = planePaths();
70
84
  fs.mkdirSync(root, { recursive: true });
71
85
  const binaryPath = writeAgentWrapper();
86
+ const orchestratorUrl = `ws://127.0.0.1:${orchestratorPort}/ws`;
72
87
  const yaml = stringify({
73
88
  version: 1,
74
89
  scalers: [{
75
90
  name: "kici-local-bare-metal",
76
91
  type: "bare-metal",
77
92
  maxAgents: 10,
78
- orchestratorUrl: `ws://127.0.0.1:${orchestratorPort}/ws`,
79
- labelSets: [
80
- {
81
- labels: ["default"],
82
- binaryPath
83
- },
84
- {
85
- labels: ["default", TRUSTED_ROUTING_LABEL],
86
- binaryPath,
87
- env: {
88
- KICI_TRUSTED_ENV: "true",
89
- KICI_SANDBOX: "false"
90
- }
91
- },
92
- {
93
- labels: [
94
- "default",
95
- TRUSTED_ROUTING_LABEL,
96
- IN_PLACE_ROUTING_LABEL
97
- ],
98
- binaryPath,
99
- env: {
100
- KICI_TRUSTED_ENV: "true",
101
- KICI_SANDBOX: "false",
102
- KICI_IN_PLACE: "true"
103
- }
93
+ orchestratorUrl,
94
+ labelSets: [{
95
+ labels: ["default"],
96
+ binaryPath
97
+ }]
98
+ }, {
99
+ name: "kici-local-bare-metal-trusted",
100
+ type: "bare-metal",
101
+ maxAgents: 10,
102
+ orchestratorUrl,
103
+ mandatoryLabels: [TRUSTED_ROUTING_LABEL],
104
+ labelSets: [{
105
+ labels: ["default", TRUSTED_ROUTING_LABEL],
106
+ binaryPath,
107
+ env: {
108
+ KICI_TRUSTED_ENV: "true",
109
+ KICI_SANDBOX: "false"
110
+ }
111
+ }, {
112
+ labels: [
113
+ "default",
114
+ TRUSTED_ROUTING_LABEL,
115
+ IN_PLACE_ROUTING_LABEL
116
+ ],
117
+ binaryPath,
118
+ env: {
119
+ KICI_TRUSTED_ENV: "true",
120
+ KICI_SANDBOX: "false",
121
+ KICI_IN_PLACE: "true"
104
122
  }
105
- ]
123
+ }]
106
124
  }]
107
125
  });
108
126
  fs.writeFileSync(scalerConfigFile, yaml);