ever-works-node 0.2.2 → 0.2.4

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 (2) hide show
  1. package/cli.js +1224 -651
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -3767,10 +3767,14 @@ var USER_SELECTABLE_WORK_KINDS = [
3767
3767
  "repo",
3768
3768
  "app"
3769
3769
  ];
3770
- var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "default"];
3770
+ var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "project", "default"];
3771
3771
  var WORK_KIND_SET = new Set(WORK_KINDS);
3772
3772
  var GOAL_KINDS = ["metric", "delivery"];
3773
3773
  var GOAL_KIND_SET = new Set(GOAL_KINDS);
3774
+ var RECORD_VISIBILITIES = ["private", "team", "organization"];
3775
+ var RECORD_VISIBILITY_SET = new Set(RECORD_VISIBILITIES);
3776
+ var RECORD_LIST_VIEWS = ["mine", "shared", "organization"];
3777
+ var RECORD_LIST_VIEW_SET = new Set(RECORD_LIST_VIEWS);
3774
3778
  var APP_DEPENDENCY_BACKUP_OVERDUE_MS = 26 * 36e5;
3775
3779
  var APP_DEPENDENCY_MANAGED = {
3776
3780
  pgRoleConnectionLimit: 20,
@@ -6119,6 +6123,145 @@ var EVER_STATS_SIGNATURE_HEADER = CONSTANTS.stats_headers.signature;
6119
6123
  var EVER_STATS_KEY_ID_HEADER = CONSTANTS.stats_headers.key_id;
6120
6124
  var EVER_STATS_SIGNATURE_PREFIX = CONSTANTS.stats_signature_prefix;
6121
6125
  var EVER_STATS_MAX_BODY_BYTES = CONSTANTS.stats.max_bytes;
6126
+ var DOMAIN_EVENT_CATALOG = [
6127
+ { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
6128
+ {
6129
+ type: "organization.member.added",
6130
+ domain: "organization",
6131
+ aggregateType: "organization",
6132
+ phase: "P1",
6133
+ child: "member"
6134
+ },
6135
+ {
6136
+ type: "organization.member.updated",
6137
+ domain: "organization",
6138
+ aggregateType: "organization",
6139
+ phase: "P1",
6140
+ child: "member"
6141
+ },
6142
+ {
6143
+ type: "organization.member.removed",
6144
+ domain: "organization",
6145
+ aggregateType: "organization",
6146
+ phase: "P1",
6147
+ child: "member"
6148
+ },
6149
+ { type: "agent.created", domain: "agents", aggregateType: "agent", phase: "P1" },
6150
+ { type: "agent.updated", domain: "agents", aggregateType: "agent", phase: "P1" },
6151
+ { type: "agent.status_changed", domain: "agents", aggregateType: "agent", phase: "P1" },
6152
+ { type: "agent.archived", domain: "agents", aggregateType: "agent", phase: "P1" },
6153
+ { type: "agent.deleted", domain: "agents", aggregateType: "agent", phase: "P1" },
6154
+ { type: "team.created", domain: "teams", aggregateType: "team", phase: "P1" },
6155
+ { type: "team.updated", domain: "teams", aggregateType: "team", phase: "P1" },
6156
+ { type: "team.deleted", domain: "teams", aggregateType: "team", phase: "P1" },
6157
+ { type: "team.member.added", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
6158
+ { type: "team.member.updated", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
6159
+ { type: "team.member.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
6160
+ { type: "team.resource.added", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
6161
+ { type: "team.resource.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
6162
+ { type: "work.created", domain: "works", aggregateType: "work", phase: "P1" },
6163
+ { type: "work.updated", domain: "works", aggregateType: "work", phase: "P1" },
6164
+ { type: "work.archived", domain: "works", aggregateType: "work", phase: "P1" },
6165
+ { type: "work.deleted", domain: "works", aggregateType: "work", phase: "P1" },
6166
+ { type: "work.member.added", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
6167
+ { type: "work.member.updated", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
6168
+ { type: "work.member.removed", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
6169
+ { type: "mission.created", domain: "missions", aggregateType: "mission", phase: "P2" },
6170
+ { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
6171
+ { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
6172
+ { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
6173
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
6174
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
6175
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
6176
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
6177
+ { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
6178
+ { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
6179
+ { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
6180
+ { type: "goal.archived", domain: "goals", aggregateType: "goal", phase: "P2" },
6181
+ { type: "goal.unarchived", domain: "goals", aggregateType: "goal", phase: "P2" },
6182
+ { type: "goal.deleted", domain: "goals", aggregateType: "goal", phase: "P2" },
6183
+ { type: "goal.progress_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
6184
+ { type: "idea.created", domain: "ideas", aggregateType: "idea", phase: "P2" },
6185
+ { type: "idea.updated", domain: "ideas", aggregateType: "idea", phase: "P2" },
6186
+ { type: "idea.status_changed", domain: "ideas", aggregateType: "idea", phase: "P2" },
6187
+ { type: "idea.deleted", domain: "ideas", aggregateType: "idea", phase: "P2" },
6188
+ { type: "task.created", domain: "tasks", aggregateType: "task", phase: "P1" },
6189
+ { type: "task.updated", domain: "tasks", aggregateType: "task", phase: "P1" },
6190
+ { type: "task.transitioned", domain: "tasks", aggregateType: "task", phase: "P1" },
6191
+ { type: "task.deleted", domain: "tasks", aggregateType: "task", phase: "P1" },
6192
+ { type: "task.assignee.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
6193
+ { type: "task.assignee.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
6194
+ { type: "task.watcher.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
6195
+ { type: "task.watcher.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
6196
+ { type: "task.block.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
6197
+ { type: "task.block.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
6198
+ { type: "task.relation.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
6199
+ { type: "task.relation.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
6200
+ { type: "task.comment.created", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
6201
+ { type: "task.comment.updated", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
6202
+ { type: "task.comment.deleted", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
6203
+ { type: "task.attachment.added", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
6204
+ { type: "task.attachment.removed", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
6205
+ { type: "task.runtime_updated", domain: "tasks", aggregateType: "task", phase: "P1", optIn: true },
6206
+ { type: "inbox.item.created", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
6207
+ { type: "inbox.item.answered", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
6208
+ { type: "inbox.item.state_changed", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
6209
+ { type: "inbox.item.deleted", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
6210
+ { type: "kb.document.created", domain: "kb", aggregateType: "kb_document", phase: "P3" },
6211
+ { type: "kb.document.updated", domain: "kb", aggregateType: "kb_document", phase: "P3" },
6212
+ { type: "kb.document.moved", domain: "kb", aggregateType: "kb_document", phase: "P3" },
6213
+ { type: "kb.document.archived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
6214
+ { type: "kb.document.unarchived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
6215
+ { type: "kb.document.deleted", domain: "kb", aggregateType: "kb_document", phase: "P3" },
6216
+ { type: "kb.document.review_changed", domain: "kb", aggregateType: "kb_document", phase: "P3" },
6217
+ { type: "kb.folder.created", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
6218
+ { type: "kb.folder.updated", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
6219
+ { type: "kb.folder.deleted", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
6220
+ { type: "email.message.received", domain: "email", aggregateType: "email_message", phase: "P4" },
6221
+ { type: "email.message.sent", domain: "email", aggregateType: "email_message", phase: "P4" },
6222
+ { type: "email.message.status_changed", domain: "email", aggregateType: "email_message", phase: "P4" },
6223
+ { type: "task.visibility_changed", domain: "visibility", aggregateType: "task", phase: "P1" },
6224
+ { type: "agent.visibility_changed", domain: "visibility", aggregateType: "agent", phase: "P1" },
6225
+ { type: "goal.visibility_changed", domain: "visibility", aggregateType: "goal", phase: "P2" },
6226
+ { type: "mission.visibility_changed", domain: "visibility", aggregateType: "mission", phase: "P2" },
6227
+ { type: "idea.visibility_changed", domain: "visibility", aggregateType: "idea", phase: "P2" }
6228
+ ];
6229
+ var DOMAIN_EVENT_TYPES = DOMAIN_EVENT_CATALOG.map((entry) => entry.type);
6230
+ var CATALOG_BY_TYPE = new Map(
6231
+ DOMAIN_EVENT_CATALOG.map((entry) => [entry.type, entry])
6232
+ );
6233
+ var INTEGRATION_SCOPES = [
6234
+ "org:read",
6235
+ "org:write",
6236
+ "members:read",
6237
+ "members:write",
6238
+ "agents:read",
6239
+ "teams:read",
6240
+ "teams:write",
6241
+ "works:read",
6242
+ "works:write",
6243
+ "missions:read",
6244
+ "missions:write",
6245
+ "goals:read",
6246
+ "goals:write",
6247
+ "ideas:read",
6248
+ "ideas:write",
6249
+ "tasks:read",
6250
+ "tasks:write",
6251
+ "inbox:read",
6252
+ "inbox:write",
6253
+ "kb:read",
6254
+ "kb:write",
6255
+ "email:read",
6256
+ "email:write",
6257
+ "events:read",
6258
+ "webhooks:manage",
6259
+ "links:write"
6260
+ ];
6261
+ var INTEGRATION_OPT_IN_SCOPES = ["org:write", "email:write"];
6262
+ var EVER_GAUZY_PRESET_SCOPES = INTEGRATION_SCOPES.filter(
6263
+ (scope) => !INTEGRATION_OPT_IN_SCOPES.includes(scope)
6264
+ );
6122
6265
 
6123
6266
  // src/core/gpu-probe.ts
6124
6267
  var VENDOR_PATTERNS = [
@@ -7232,272 +7375,609 @@ function readPendingComputerSessions(payload) {
7232
7375
  }
7233
7376
  __name(readPendingComputerSessions, "readPendingComputerSessions");
7234
7377
 
7235
- // src/core/api-base.ts
7236
- var API_URL_ENV = "EVER_WORKS_NODE_API_URL";
7237
- function rawPin(env) {
7238
- const value = env[API_URL_ENV];
7239
- if (typeof value !== "string") {
7378
+ // src/core/heartbeat.ts
7379
+ var systemScheduler = {
7380
+ setTimeout: /* @__PURE__ */ __name((callback, ms) => setTimeout(callback, ms), "setTimeout"),
7381
+ clearTimeout: /* @__PURE__ */ __name((handle) => clearTimeout(handle), "clearTimeout")
7382
+ };
7383
+ var OPTIONAL_DESCRIPTION_FIELD_TIERS = [
7384
+ // NEWEST first. Node lifecycle + remote node limits (self-build slices
7385
+ // AR, AS). Tiered (review): `cliVersions` goes out on EVERY beat of a
7386
+ // node with a pinned CLI, so an upgraded node talking to a platform that
7387
+ // predates it would otherwise drop the worker state and housekeeping
7388
+ // along with it — losing health signals that platform DOES understand,
7389
+ // for the life of the process. Stripping the newest tier first keeps them.
7390
+ ["cliVersions", "maxConcurrentJobs", "maxCpuPercent", "maxMemoryMb"],
7391
+ // Fleet health signals (EW-776) and node housekeeping (EW-803).
7392
+ [
7393
+ "workerState",
7394
+ "workerStateReason",
7395
+ "minFreeDiskBytes",
7396
+ "workspaceCount",
7397
+ "workspaceBytes",
7398
+ "lastReclaimAt",
7399
+ "lastReclaimFreedBytes"
7400
+ ]
7401
+ ];
7402
+ function untilHeartbeatAccepted(loop, shouldWait = () => true) {
7403
+ const verdict = /* @__PURE__ */ __name((state) => {
7404
+ if (state.state === "connected") return true;
7405
+ if (state.state === "stopped" || !shouldWait()) return false;
7240
7406
  return null;
7241
- }
7242
- const trimmed = value.trim();
7243
- return trimmed ? trimmed : null;
7407
+ }, "verdict");
7408
+ const now = verdict(loop.getState());
7409
+ if (now !== null) return Promise.resolve(now);
7410
+ return new Promise((resolve9) => {
7411
+ const unsubscribe = loop.onChange((state) => {
7412
+ const settled = verdict(state);
7413
+ if (settled === null) return;
7414
+ unsubscribe();
7415
+ resolve9(settled);
7416
+ });
7417
+ });
7244
7418
  }
7245
- __name(rawPin, "rawPin");
7246
- function readApiUrlPin(env = process.env) {
7247
- const raw = rawPin(env);
7248
- return raw === null ? null : normalizeApiUrl(raw);
7419
+ __name(untilHeartbeatAccepted, "untilHeartbeatAccepted");
7420
+ function computeBackoffDelay(intervalMs, failures, maxMs) {
7421
+ if (failures <= 0) {
7422
+ return intervalMs;
7423
+ }
7424
+ const exponent = Math.min(failures, 30);
7425
+ const delay2 = intervalMs * 2 ** exponent;
7426
+ return Math.min(delay2, maxMs);
7249
7427
  }
7250
- __name(readApiUrlPin, "readApiUrlPin");
7251
- function resolveApiBase(config, env = process.env) {
7252
- const configuredUrl = config.apiUrl;
7253
- const pinnedTo = readApiUrlPin(env);
7254
- if (pinnedTo === null) {
7255
- return { url: configuredUrl, source: "config", configuredUrl, pinnedTo: null, mismatch: false };
7428
+ __name(computeBackoffDelay, "computeBackoffDelay");
7429
+ var HeartbeatLoop = class {
7430
+ static {
7431
+ __name(this, "HeartbeatLoop");
7256
7432
  }
7257
- return {
7258
- url: pinnedTo,
7259
- source: "pin",
7260
- configuredUrl,
7261
- pinnedTo,
7262
- mismatch: pinnedTo !== configuredUrl
7433
+ options;
7434
+ scheduler;
7435
+ now;
7436
+ listeners = /* @__PURE__ */ new Set();
7437
+ timer = null;
7438
+ running = false;
7439
+ inFlight = null;
7440
+ /**
7441
+ * How many optional-field TIERS (newest first) this platform has proven
7442
+ * it predates; from then on the beat leaves those tiers out. 0 = it
7443
+ * takes everything. Process-scoped on purpose — a platform upgrade is a
7444
+ * restart-shaped event for the node, and re-probing on every beat would
7445
+ * mean one wasted round trip per beat, forever, against an API that
7446
+ * will never accept them.
7447
+ */
7448
+ strippedTiers = 0;
7449
+ state = {
7450
+ state: "idle",
7451
+ lastHeartbeatAt: null,
7452
+ consecutiveFailures: 0,
7453
+ nextAttemptInMs: null,
7454
+ lastError: null,
7455
+ lastErrorKind: null,
7456
+ node: null
7263
7457
  };
7264
- }
7265
- __name(resolveApiBase, "resolveApiBase");
7266
- function describeApiBase(base) {
7267
- if (base.source === "config") {
7268
- return `${base.url} (from the enrolled config)`;
7458
+ constructor(options) {
7459
+ this.options = {
7460
+ ...options,
7461
+ intervalMs: options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
7462
+ maxBackoffMs: options.maxBackoffMs ?? MAX_HEARTBEAT_BACKOFF_MS
7463
+ };
7464
+ this.scheduler = options.scheduler ?? systemScheduler;
7465
+ this.now = options.now ?? (() => Date.now());
7466
+ this.options.logger?.protect(options.secret);
7269
7467
  }
7270
- if (!base.mismatch) {
7271
- return `${base.url} (pinned via ${API_URL_ENV}; matches the enrolled origin)`;
7468
+ getState() {
7469
+ return { ...this.state };
7272
7470
  }
7273
- return `${base.url} (PINNED via ${API_URL_ENV}) \u2014 enrolled against ${base.configuredUrl}. The pin does not match the enrolled origin: this node has no credential on the pinned platform, so every call will be refused with 401 until the pin is corrected or the node is re-enrolled.`;
7274
- }
7275
- __name(describeApiBase, "describeApiBase");
7276
-
7277
- // src/core/auth-client.ts
7278
- var EVER_ID_DEVICE_SCOPE = ["openid", "email", EVER_ID_SCOPES.SESSION_EXCHANGE].join(" ");
7279
-
7280
- // src/core/telemetry-probe.ts
7281
- var AGENT_CLI_CANDIDATES = ["claude", "codex", "gemini", "opencode"];
7282
- function parseCliVersion(stdout) {
7283
- if (typeof stdout !== "string") return null;
7284
- const match = /\b(\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?)\b/.exec(stdout);
7285
- return match ? match[1] : null;
7286
- }
7287
- __name(parseCliVersion, "parseCliVersion");
7288
- async function detectAgentCliVersion(runner, candidates = AGENT_CLI_CANDIDATES) {
7289
- for (const command of candidates) {
7290
- try {
7291
- const result = await runner.run(command, ["--version"]);
7292
- if (result.code !== 0) continue;
7293
- const version = parseCliVersion(`${result.stdout ?? ""}
7294
- ${result.stderr ?? ""}`);
7295
- if (!version) continue;
7296
- return `${command} ${version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH);
7297
- } catch {
7298
- continue;
7471
+ onChange(listener) {
7472
+ this.listeners.add(listener);
7473
+ return () => {
7474
+ this.listeners.delete(listener);
7475
+ };
7476
+ }
7477
+ /** Start beating. Resolves once the FIRST beat has settled. */
7478
+ start() {
7479
+ if (this.running) {
7480
+ return this.inFlight ?? Promise.resolve();
7299
7481
  }
7482
+ this.running = true;
7483
+ return this.tick();
7300
7484
  }
7301
- return null;
7302
- }
7303
- __name(detectAgentCliVersion, "detectAgentCliVersion");
7304
- async function detectDiskFreeBytes(io, path2) {
7305
- try {
7306
- const free = await io.freeBytes(path2);
7307
- if (typeof free !== "number" || !Number.isFinite(free) || free < 0) return null;
7308
- return Math.floor(free);
7309
- } catch {
7310
- return null;
7485
+ /** Stop beating and cancel any pending timer. */
7486
+ stop() {
7487
+ this.running = false;
7488
+ this.cancelTimer();
7489
+ this.patch({ state: "stopped", nextAttemptInMs: null });
7311
7490
  }
7312
- }
7313
- __name(detectDiskFreeBytes, "detectDiskFreeBytes");
7314
- var MODEL_IDENTITY_CACHE_TTL_MS = 5 * 6e4;
7315
- function parseClaudeAuthStatus(stdout) {
7316
- let parsed;
7317
- try {
7318
- parsed = JSON.parse(stdout.trim());
7319
- } catch {
7320
- return null;
7491
+ /** Await the in-flight beat, if any (test/shutdown helper). */
7492
+ async settled() {
7493
+ await this.inFlight;
7321
7494
  }
7322
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
7323
- const status = parsed;
7324
- if (status.loggedIn !== true) return "claude-code: not logged in";
7325
- const email = compactString(status.email);
7326
- const org = compactString(status.orgName);
7327
- const plan = compactString(status.subscriptionType);
7328
- const method = compactString(status.authMethod);
7329
- const who = email ?? (method ? `${method} login` : "logged in");
7330
- const details = [org, plan].filter((part) => part !== null);
7331
- return `claude-code: ${who}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
7332
- }
7333
- __name(parseClaudeAuthStatus, "parseClaudeAuthStatus");
7334
- function parseCodexLoginStatus(stdout) {
7335
- const text = stdout.trim().toLowerCase();
7336
- if (!text) return null;
7337
- if (text.includes("not logged in")) return "codex: not logged in";
7338
- if (text.includes("chatgpt")) return "codex: chatgpt";
7339
- if (text.includes("api key")) return "codex: api-key";
7340
- if (text.includes("logged in")) return "codex: logged in";
7341
- return null;
7342
- }
7343
- __name(parseCodexLoginStatus, "parseCodexLoginStatus");
7344
- function compactString(value) {
7345
- if (typeof value !== "string") return null;
7346
- const cleaned = value.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
7347
- return cleaned ? cleaned : null;
7348
- }
7349
- __name(compactString, "compactString");
7350
- async function detectModelIdentity(runner, paths = {}) {
7351
- const candidates = [
7352
- { command: paths["claude-code"] || "claude", args: ["auth", "status", "--json"], parse: parseClaudeAuthStatus },
7353
- { command: paths.codex || "codex", args: ["login", "status"], parse: parseCodexLoginStatus }
7354
- ];
7355
- for (const candidate of candidates) {
7495
+ /** Run one beat now. Exposed so the UI can offer "retry" and tests can step. */
7496
+ tick() {
7497
+ const run = this.runOnce().finally(() => {
7498
+ if (this.inFlight === run) {
7499
+ this.inFlight = null;
7500
+ }
7501
+ });
7502
+ this.inFlight = run;
7503
+ return run;
7504
+ }
7505
+ async runOnce() {
7506
+ if (!this.running) {
7507
+ return;
7508
+ }
7509
+ this.cancelTimer();
7510
+ if (this.state.state !== "connected") {
7511
+ this.patch({ state: "connecting" });
7512
+ }
7356
7513
  try {
7357
- const result = await runner.run(candidate.command, candidate.args);
7358
- const stdout = result.stdout ?? "";
7359
- const stderr = result.stderr ?? "";
7360
- const label = candidate.parse(stdout) ?? candidate.parse(`${stdout}
7361
- ${stderr}`);
7362
- if (label) return label.slice(0, FLEET_MAX_MODEL_IDENTITY_LENGTH);
7363
- } catch {
7364
- continue;
7514
+ const description = await this.options.describe();
7515
+ this.onSuccess(await this.beat(description));
7516
+ } catch (error) {
7517
+ this.onFailure(error);
7365
7518
  }
7519
+ this.scheduleNext();
7366
7520
  }
7367
- return null;
7368
- }
7369
- __name(detectModelIdentity, "detectModelIdentity");
7370
- function cacheProbe(probe, ttlMs = MODEL_IDENTITY_CACHE_TTL_MS, now = Date.now) {
7371
- let value = null;
7372
- let expiresAt = Number.NEGATIVE_INFINITY;
7373
- let inFlight = null;
7374
- return async () => {
7375
- if (now() < expiresAt) return value;
7376
- if (inFlight) return inFlight;
7377
- inFlight = probe().then((fresh) => {
7378
- value = fresh;
7379
- expiresAt = now() + ttlMs;
7380
- return fresh;
7381
- }).finally(() => {
7382
- inFlight = null;
7521
+ /**
7522
+ * Send one beat, tolerating a platform that predates the optional
7523
+ * self-description fields (the worker state, EW-776; the housekeeping
7524
+ * report, EW-803).
7525
+ *
7526
+ * The API validates heartbeats with `whitelist + forbidNonWhitelisted`,
7527
+ * so a field an older build does not know is not ignored — it 400s the
7528
+ * whole request. A 400 is a FAILED beat, a failed beat backs off, and
7529
+ * enough of them sweep the node to `offline`. Which would mean shipping
7530
+ * a health-reporting feature whose failure mode is "every node in a
7531
+ * mixed-version fleet goes dark": the exact class of outage this slice
7532
+ * was written to end.
7533
+ *
7534
+ * So a 400 on a beat that CARRIED those fields is retried once,
7535
+ * immediately, without them. If that succeeds the platform is simply
7536
+ * older than this daemon; we latch, say so once, and keep reporting
7537
+ * liveness. The retry costs one request, once per process.
7538
+ *
7539
+ * Inferring "the field was rejected" from a bare 400 is deliberate and
7540
+ * has a cost worth stating: `FleetClient` never surfaces server bodies
7541
+ * (fixed client-authored messages only), so a 400 caused by something
7542
+ * else on such a beat also latches, and this node reports liveness only
7543
+ * until it restarts. That is the safe direction — the node stays
7544
+ * observable either way, and only the extra signal is lost.
7545
+ */
7546
+ async beat(description) {
7547
+ const credential = { nodeId: this.options.nodeId, secret: this.options.secret };
7548
+ let sent = stripTiers(description, this.strippedTiers);
7549
+ try {
7550
+ return await this.options.client.heartbeat({ ...credential, ...sent });
7551
+ } catch (error) {
7552
+ if (!isFieldRejection(error)) throw error;
7553
+ let lastError = error;
7554
+ for (let tiers = this.strippedTiers + 1; tiers <= OPTIONAL_DESCRIPTION_FIELD_TIERS.length; tiers += 1) {
7555
+ const candidate = stripTiers(description, tiers);
7556
+ if (Object.keys(candidate).length === Object.keys(sent).length) continue;
7557
+ sent = candidate;
7558
+ try {
7559
+ const result = await this.options.client.heartbeat({ ...credential, ...candidate });
7560
+ this.strippedTiers = tiers;
7561
+ this.options.logger?.warn(
7562
+ tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length ? "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts." : "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts."
7563
+ );
7564
+ return result;
7565
+ } catch (retryError) {
7566
+ if (!isFieldRejection(retryError)) throw retryError;
7567
+ lastError = retryError;
7568
+ }
7569
+ }
7570
+ throw lastError;
7571
+ }
7572
+ }
7573
+ onSuccess(result) {
7574
+ this.patch({
7575
+ state: "connected",
7576
+ lastHeartbeatAt: this.now(),
7577
+ consecutiveFailures: 0,
7578
+ lastError: null,
7579
+ lastErrorKind: null,
7580
+ node: result.node
7383
7581
  });
7384
- return inFlight;
7385
- };
7386
- }
7387
- __name(cacheProbe, "cacheProbe");
7388
-
7389
- // src/core/executors/model-cli.ts
7390
- var MODEL_CLI_EMPTY_MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
7391
- `;
7392
- function modelCliMcpIsolationEnv(provider) {
7393
- return provider === "claude-code" ? { ENABLE_CLAUDEAI_MCP_SERVERS: "false" } : null;
7394
- }
7395
- __name(modelCliMcpIsolationEnv, "modelCliMcpIsolationEnv");
7396
- function claudeManagedMcpConfigPath(platform = process.platform) {
7397
- if (platform === "win32") return "C:\\Program Files\\ClaudeCode\\managed-mcp.json";
7398
- if (platform === "darwin") return "/Library/Application Support/ClaudeCode/managed-mcp.json";
7399
- return "/etc/claude-code/managed-mcp.json";
7400
- }
7401
- __name(claudeManagedMcpConfigPath, "claudeManagedMcpConfigPath");
7402
- function claudeManagedMcpConfigInEffect(raw) {
7403
- if (typeof raw !== "string" || !raw.trim()) return false;
7404
- try {
7405
- const parsed = JSON.parse(raw);
7406
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
7407
- } catch {
7408
- return false;
7582
+ if (this.options.onAccepted) {
7583
+ try {
7584
+ this.options.onAccepted(result);
7585
+ } catch {
7586
+ }
7587
+ }
7409
7588
  }
7410
- }
7411
- __name(claudeManagedMcpConfigInEffect, "claudeManagedMcpConfigInEffect");
7412
- var ModelCliCommandError = class extends Error {
7413
- static {
7414
- __name(this, "ModelCliCommandError");
7589
+ onFailure(error) {
7590
+ const kind = error instanceof FleetClientError ? error.kind : "network";
7591
+ const raw = error instanceof Error ? error.message : String(error);
7592
+ const message = this.options.logger?.redact(raw) ?? raw;
7593
+ const failures = this.state.consecutiveFailures + 1;
7594
+ this.patch({
7595
+ // `unauthorized` is sticky and visible: the operator has to act
7596
+ // (re-enable the node in Fleet, or re-enroll). We keep retrying at
7597
+ // the backoff ceiling so a re-enabled node recovers on its own.
7598
+ state: kind === "unauthorized" ? "unauthorized" : "retrying",
7599
+ consecutiveFailures: failures,
7600
+ lastError: message,
7601
+ lastErrorKind: kind
7602
+ });
7603
+ this.options.logger?.warn(`Heartbeat failed (attempt ${failures}): ${message}`);
7415
7604
  }
7416
- constructor(message) {
7417
- super(message);
7418
- this.name = "ModelCliCommandError";
7605
+ scheduleNext() {
7606
+ if (!this.running) {
7607
+ return;
7608
+ }
7609
+ const delay2 = computeBackoffDelay(
7610
+ this.options.intervalMs,
7611
+ this.state.consecutiveFailures,
7612
+ this.options.maxBackoffMs
7613
+ );
7614
+ this.patch({ nextAttemptInMs: delay2 });
7615
+ this.timer = this.scheduler.setTimeout(() => {
7616
+ this.timer = null;
7617
+ void this.tick();
7618
+ }, delay2);
7419
7619
  }
7420
- };
7421
- var MODEL_CLI_STEP_ID = "model";
7422
- var MODEL_CLI_OUTPUT_TAIL_BYTES = 8 * 1024;
7423
- var MODEL_CLI_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
7424
- var ABSOLUTE_WIN32 = /^[A-Za-z]:[\\/]/;
7425
- var UNSAFE_PATH_CHARS = /[\0\r\n"'`$&|;<>%!^]/;
7426
- function quoteShellPath(path2, platform = process.platform) {
7427
- if (typeof path2 !== "string" || !path2.trim()) {
7428
- throw new ModelCliCommandError("Path must not be empty");
7620
+ cancelTimer() {
7621
+ if (this.timer !== null) {
7622
+ this.scheduler.clearTimeout(this.timer);
7623
+ this.timer = null;
7624
+ }
7429
7625
  }
7430
- if (UNSAFE_PATH_CHARS.test(path2)) {
7431
- throw new ModelCliCommandError("Path contains characters the shell could interpret");
7626
+ patch(next) {
7627
+ this.state = { ...this.state, ...next };
7628
+ const snapshot = this.getState();
7629
+ for (const listener of this.listeners) {
7630
+ listener(snapshot);
7631
+ }
7432
7632
  }
7433
- const absolute = platform === "win32" ? ABSOLUTE_WIN32.test(path2) || path2.startsWith("\\\\") : path2.startsWith("/");
7434
- if (!absolute) {
7435
- throw new ModelCliCommandError("Path must be absolute");
7633
+ };
7634
+ function stripTiers(description, tiers) {
7635
+ const out = {};
7636
+ const dropped = new Set(OPTIONAL_DESCRIPTION_FIELD_TIERS.slice(0, tiers).flat());
7637
+ for (const [key, value] of Object.entries(description)) {
7638
+ if (value === void 0 || dropped.has(key)) continue;
7639
+ out[key] = value;
7436
7640
  }
7437
- return `"${path2}"`;
7641
+ return out;
7438
7642
  }
7439
- __name(quoteShellPath, "quoteShellPath");
7440
- function assertMcpServerName(name) {
7441
- if (typeof name !== "string" || !MCP_SERVER_NAME_PATTERN.test(name)) {
7442
- throw new ModelCliCommandError("MCP server name is not an opaque identifier");
7443
- }
7444
- return name;
7643
+ __name(stripTiers, "stripTiers");
7644
+ function isFieldRejection(error) {
7645
+ return error instanceof FleetClientError && error.kind === "invalid-request" && error.status === 400;
7445
7646
  }
7446
- __name(assertMcpServerName, "assertMcpServerName");
7447
- function assertLoopbackUrl(url) {
7448
- if (typeof url !== "string" || !MCP_LOOPBACK_URL_PATTERN.test(url)) {
7449
- throw new ModelCliCommandError("MCP bridge URL is not a loopback URL this node produced");
7647
+ __name(isFieldRejection, "isFieldRejection");
7648
+
7649
+ // src/core/api-base.ts
7650
+ var API_URL_ENV = "EVER_WORKS_NODE_API_URL";
7651
+ function rawPin(env) {
7652
+ const value = env[API_URL_ENV];
7653
+ if (typeof value !== "string") {
7654
+ return null;
7450
7655
  }
7451
- return url;
7656
+ const trimmed = value.trim();
7657
+ return trimmed ? trimmed : null;
7452
7658
  }
7453
- __name(assertLoopbackUrl, "assertLoopbackUrl");
7454
- var MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
7455
- var MCP_LOOPBACK_URL_PATTERN = /^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp\/[a-f0-9]{8,64}$/;
7456
- function assertModelId(model) {
7457
- if (model === void 0) return null;
7458
- if (!FLEET_AGENT_EXECUTION_MODEL_PATTERN.test(model)) {
7459
- throw new ModelCliCommandError("Model id is not an opaque identifier");
7460
- }
7461
- return model;
7659
+ __name(rawPin, "rawPin");
7660
+ function readApiUrlPin(env = process.env) {
7661
+ const raw = rawPin(env);
7662
+ return raw === null ? null : normalizeApiUrl(raw);
7462
7663
  }
7463
- __name(assertModelId, "assertModelId");
7464
- var MODEL_CLI_OPTIONAL_FLAGS = {
7465
- "claude-code": ["--effort", "--max-budget-usd"],
7466
- codex: []
7467
- };
7468
- function unsupportedOptionalModelCliFlags(execution, supportedFlags) {
7469
- if (!supportedFlags) return [];
7470
- const wanted = [];
7471
- if (execution.provider === "claude-code") {
7472
- if (execution.effort) wanted.push("--effort");
7473
- if (execution.maxBudgetUsd !== void 0) wanted.push("--max-budget-usd");
7664
+ __name(readApiUrlPin, "readApiUrlPin");
7665
+ function resolveApiBase(config, env = process.env) {
7666
+ const configuredUrl = config.apiUrl;
7667
+ const pinnedTo = readApiUrlPin(env);
7668
+ if (pinnedTo === null) {
7669
+ return { url: configuredUrl, source: "config", configuredUrl, pinnedTo: null, mismatch: false };
7474
7670
  }
7475
- const droppable = new Set(
7476
- MODEL_CLI_OPTIONAL_FLAGS[execution.provider] ?? []
7477
- );
7478
- return wanted.filter((flag) => droppable.has(flag) && !supportedFlags.has(flag));
7671
+ return {
7672
+ url: pinnedTo,
7673
+ source: "pin",
7674
+ configuredUrl,
7675
+ pinnedTo,
7676
+ mismatch: pinnedTo !== configuredUrl
7677
+ };
7479
7678
  }
7480
- __name(unsupportedOptionalModelCliFlags, "unsupportedOptionalModelCliFlags");
7481
- function assertResumableSession(provider, sessionId) {
7482
- if (sessionId === void 0) return null;
7483
- if (!fleetAgentExecutionProviderSupportsSessionResume(provider)) {
7484
- throw new ModelCliCommandError(`Provider '${String(provider)}' cannot resume a CLI session on the fleet`);
7485
- }
7486
- if (!isFleetAgentModelSessionId(sessionId)) {
7487
- throw new ModelCliCommandError("CLI session id is not an opaque identifier");
7679
+ __name(resolveApiBase, "resolveApiBase");
7680
+ function describeApiBase(base) {
7681
+ if (base.source === "config") {
7682
+ return `${base.url} (from the enrolled config)`;
7488
7683
  }
7489
- return sessionId;
7490
- }
7491
- __name(assertResumableSession, "assertResumableSession");
7492
- function formatBudget(value) {
7493
- if (value === void 0) return null;
7494
- if (!Number.isFinite(value) || value <= 0) {
7495
- throw new ModelCliCommandError("maxBudgetUsd must be a positive number");
7684
+ if (!base.mismatch) {
7685
+ return `${base.url} (pinned via ${API_URL_ENV}; matches the enrolled origin)`;
7496
7686
  }
7497
- return String(Math.round(value * 100) / 100);
7687
+ return `${base.url} (PINNED via ${API_URL_ENV}) \u2014 enrolled against ${base.configuredUrl}. The pin does not match the enrolled origin: this node has no credential on the pinned platform, so every call will be refused with 401 until the pin is corrected or the node is re-enrolled.`;
7498
7688
  }
7499
- __name(formatBudget, "formatBudget");
7500
- function buildModelCliCommand(input) {
7689
+ __name(describeApiBase, "describeApiBase");
7690
+
7691
+ // src/core/auth-client.ts
7692
+ var EVER_ID_DEVICE_SCOPE = ["openid", "email", EVER_ID_SCOPES.SESSION_EXCHANGE].join(" ");
7693
+
7694
+ // src/core/telemetry-probe.ts
7695
+ var AGENT_CLI_CANDIDATES = ["claude", "codex", "gemini", "opencode"];
7696
+ function parseCliVersion(stdout) {
7697
+ if (typeof stdout !== "string") return null;
7698
+ const match = /\b(\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?)\b/.exec(stdout);
7699
+ return match ? match[1] : null;
7700
+ }
7701
+ __name(parseCliVersion, "parseCliVersion");
7702
+ async function detectAgentCliVersion(runner, candidates = AGENT_CLI_CANDIDATES) {
7703
+ for (const command of candidates) {
7704
+ try {
7705
+ const result = await runner.run(command, ["--version"]);
7706
+ if (result.code !== 0) continue;
7707
+ const version = parseCliVersion(`${result.stdout ?? ""}
7708
+ ${result.stderr ?? ""}`);
7709
+ if (!version) continue;
7710
+ return `${command} ${version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH);
7711
+ } catch {
7712
+ continue;
7713
+ }
7714
+ }
7715
+ return null;
7716
+ }
7717
+ __name(detectAgentCliVersion, "detectAgentCliVersion");
7718
+ async function detectDiskFreeBytes(io, path2) {
7719
+ try {
7720
+ const free = await io.freeBytes(path2);
7721
+ if (typeof free !== "number" || !Number.isFinite(free) || free < 0) return null;
7722
+ return Math.floor(free);
7723
+ } catch {
7724
+ return null;
7725
+ }
7726
+ }
7727
+ __name(detectDiskFreeBytes, "detectDiskFreeBytes");
7728
+ var MODEL_IDENTITY_CACHE_TTL_MS = 5 * 6e4;
7729
+ function parseClaudeAuthStatus(stdout) {
7730
+ let parsed;
7731
+ try {
7732
+ parsed = JSON.parse(stdout.trim());
7733
+ } catch {
7734
+ return null;
7735
+ }
7736
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
7737
+ const status = parsed;
7738
+ if (status.loggedIn !== true) return "claude-code: not logged in";
7739
+ const email = compactString(status.email);
7740
+ const org = compactString(status.orgName);
7741
+ const plan = compactString(status.subscriptionType);
7742
+ const method = compactString(status.authMethod);
7743
+ const who = email ?? (method ? `${method} login` : "logged in");
7744
+ const details = [org, plan].filter((part) => part !== null);
7745
+ return `claude-code: ${who}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
7746
+ }
7747
+ __name(parseClaudeAuthStatus, "parseClaudeAuthStatus");
7748
+ function parseCodexLoginStatus(stdout) {
7749
+ const text = stdout.trim().toLowerCase();
7750
+ if (!text) return null;
7751
+ if (text.includes("not logged in")) return "codex: not logged in";
7752
+ if (text.includes("chatgpt")) return "codex: chatgpt";
7753
+ if (text.includes("api key")) return "codex: api-key";
7754
+ if (text.includes("logged in")) return "codex: logged in";
7755
+ return null;
7756
+ }
7757
+ __name(parseCodexLoginStatus, "parseCodexLoginStatus");
7758
+ function compactString(value) {
7759
+ if (typeof value !== "string") return null;
7760
+ const cleaned = value.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
7761
+ return cleaned ? cleaned : null;
7762
+ }
7763
+ __name(compactString, "compactString");
7764
+ async function detectModelIdentity(runner, paths = {}) {
7765
+ const candidates = [
7766
+ { command: paths["claude-code"] || "claude", args: ["auth", "status", "--json"], parse: parseClaudeAuthStatus },
7767
+ { command: paths.codex || "codex", args: ["login", "status"], parse: parseCodexLoginStatus }
7768
+ ];
7769
+ for (const candidate of candidates) {
7770
+ try {
7771
+ const result = await runner.run(candidate.command, candidate.args);
7772
+ const stdout = result.stdout ?? "";
7773
+ const stderr = result.stderr ?? "";
7774
+ const label = candidate.parse(stdout) ?? candidate.parse(`${stdout}
7775
+ ${stderr}`);
7776
+ if (label) return label.slice(0, FLEET_MAX_MODEL_IDENTITY_LENGTH);
7777
+ } catch {
7778
+ continue;
7779
+ }
7780
+ }
7781
+ return null;
7782
+ }
7783
+ __name(detectModelIdentity, "detectModelIdentity");
7784
+ function cacheProbe(probe, ttlMs = MODEL_IDENTITY_CACHE_TTL_MS, now = Date.now) {
7785
+ let value = null;
7786
+ let expiresAt = Number.NEGATIVE_INFINITY;
7787
+ let inFlight = null;
7788
+ return async () => {
7789
+ if (now() < expiresAt) return value;
7790
+ if (inFlight) return inFlight;
7791
+ inFlight = probe().then((fresh) => {
7792
+ value = fresh;
7793
+ expiresAt = now() + ttlMs;
7794
+ return fresh;
7795
+ }).finally(() => {
7796
+ inFlight = null;
7797
+ });
7798
+ return inFlight;
7799
+ };
7800
+ }
7801
+ __name(cacheProbe, "cacheProbe");
7802
+
7803
+ // src/core/executors/model-cli.ts
7804
+ var MODEL_CLI_EMPTY_MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
7805
+ `;
7806
+ var CODEX_SETTING_VALUE = /^[A-Za-z0-9._-]{1,64}$/;
7807
+ var TOML_STRING_ASSIGNMENT = /^([A-Za-z0-9_-]+)[ \t]*=[ \t]*(?:"([^"\\]*)"|'([^']*)')(?:[ \t]+#.*|#.*)?$/;
7808
+ var TOML_MULTILINE_VALUE_START = /^[^=#]*=[ \t]*("""|''')/;
7809
+ function readCodexUserSettings(configToml) {
7810
+ const settings = {};
7811
+ if (typeof configToml !== "string" || configToml.length === 0) return settings;
7812
+ let table = "";
7813
+ let openMultiline = null;
7814
+ for (const raw of configToml.split(/\r?\n/)) {
7815
+ if (openMultiline !== null) {
7816
+ if (indexOfTomlClosingDelimiter(raw, openMultiline) !== -1) openMultiline = null;
7817
+ continue;
7818
+ }
7819
+ const line = raw.trim();
7820
+ const opening = TOML_MULTILINE_VALUE_START.exec(line);
7821
+ if (opening) {
7822
+ const delimiter = opening[1];
7823
+ if (indexOfTomlClosingDelimiter(line.slice(opening[0].length), delimiter) === -1) {
7824
+ openMultiline = delimiter;
7825
+ continue;
7826
+ }
7827
+ }
7828
+ if (line.startsWith("[")) {
7829
+ if (line.startsWith("[[")) {
7830
+ table = "\0array";
7831
+ } else {
7832
+ const close = line.indexOf("]");
7833
+ table = close > 1 ? line.slice(1, close).trim() : "\0unknown";
7834
+ }
7835
+ continue;
7836
+ }
7837
+ const assignment = TOML_STRING_ASSIGNMENT.exec(line);
7838
+ if (!assignment) continue;
7839
+ const key = assignment[1];
7840
+ const value = assignment[2] ?? assignment[3] ?? "";
7841
+ if (!CODEX_SETTING_VALUE.test(value)) continue;
7842
+ if (table === "" && key === "model" && settings.model === void 0) settings.model = value;
7843
+ else if (table === "" && key === "model_reasoning_effort" && settings.reasoningEffort === void 0) {
7844
+ settings.reasoningEffort = value;
7845
+ } else if (table === "windows" && key === "sandbox" && settings.windowsSandbox === void 0) {
7846
+ settings.windowsSandbox = value;
7847
+ }
7848
+ }
7849
+ return settings;
7850
+ }
7851
+ __name(readCodexUserSettings, "readCodexUserSettings");
7852
+ function indexOfTomlClosingDelimiter(text, delimiter) {
7853
+ let from = 0;
7854
+ for (; ; ) {
7855
+ const at = text.indexOf(delimiter, from);
7856
+ if (at === -1 || delimiter === "'''") return at;
7857
+ let backslashes = 0;
7858
+ for (let i = at - 1; i >= 0 && text[i] === "\\"; i -= 1) backslashes += 1;
7859
+ if (backslashes % 2 === 0) return at;
7860
+ from = at + 1;
7861
+ }
7862
+ }
7863
+ __name(indexOfTomlClosingDelimiter, "indexOfTomlClosingDelimiter");
7864
+ function assertCodexSetting(name, value) {
7865
+ if (value === void 0) return void 0;
7866
+ if (!CODEX_SETTING_VALUE.test(value)) {
7867
+ throw new ModelCliCommandError(`Refusing to forward the Codex setting '${name}': not a bare identifier`);
7868
+ }
7869
+ return value;
7870
+ }
7871
+ __name(assertCodexSetting, "assertCodexSetting");
7872
+ function modelCliMcpIsolationEnv(provider) {
7873
+ return provider === "claude-code" ? { ENABLE_CLAUDEAI_MCP_SERVERS: "false" } : null;
7874
+ }
7875
+ __name(modelCliMcpIsolationEnv, "modelCliMcpIsolationEnv");
7876
+ function claudeManagedMcpConfigPath(platform = process.platform) {
7877
+ if (platform === "win32") return "C:\\Program Files\\ClaudeCode\\managed-mcp.json";
7878
+ if (platform === "darwin") return "/Library/Application Support/ClaudeCode/managed-mcp.json";
7879
+ return "/etc/claude-code/managed-mcp.json";
7880
+ }
7881
+ __name(claudeManagedMcpConfigPath, "claudeManagedMcpConfigPath");
7882
+ function claudeManagedMcpConfigInEffect(raw) {
7883
+ if (typeof raw !== "string" || !raw.trim()) return false;
7884
+ try {
7885
+ const parsed = JSON.parse(raw);
7886
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
7887
+ } catch {
7888
+ return false;
7889
+ }
7890
+ }
7891
+ __name(claudeManagedMcpConfigInEffect, "claudeManagedMcpConfigInEffect");
7892
+ var ModelCliCommandError = class extends Error {
7893
+ static {
7894
+ __name(this, "ModelCliCommandError");
7895
+ }
7896
+ constructor(message) {
7897
+ super(message);
7898
+ this.name = "ModelCliCommandError";
7899
+ }
7900
+ };
7901
+ var MODEL_CLI_STEP_ID = "model";
7902
+ var MODEL_CLI_OUTPUT_TAIL_BYTES = 8 * 1024;
7903
+ var MODEL_CLI_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
7904
+ var ABSOLUTE_WIN32 = /^[A-Za-z]:[\\/]/;
7905
+ var UNSAFE_PATH_CHARS = /[\0\r\n"'`$&|;<>%!^]/;
7906
+ function quoteShellPath(path2, platform = process.platform) {
7907
+ if (typeof path2 !== "string" || !path2.trim()) {
7908
+ throw new ModelCliCommandError("Path must not be empty");
7909
+ }
7910
+ if (UNSAFE_PATH_CHARS.test(path2)) {
7911
+ throw new ModelCliCommandError("Path contains characters the shell could interpret");
7912
+ }
7913
+ const absolute = platform === "win32" ? ABSOLUTE_WIN32.test(path2) || path2.startsWith("\\\\") : path2.startsWith("/");
7914
+ if (!absolute) {
7915
+ throw new ModelCliCommandError("Path must be absolute");
7916
+ }
7917
+ return `"${path2}"`;
7918
+ }
7919
+ __name(quoteShellPath, "quoteShellPath");
7920
+ function assertMcpServerName(name) {
7921
+ if (typeof name !== "string" || !MCP_SERVER_NAME_PATTERN.test(name)) {
7922
+ throw new ModelCliCommandError("MCP server name is not an opaque identifier");
7923
+ }
7924
+ return name;
7925
+ }
7926
+ __name(assertMcpServerName, "assertMcpServerName");
7927
+ function assertLoopbackUrl(url) {
7928
+ if (typeof url !== "string" || !MCP_LOOPBACK_URL_PATTERN.test(url)) {
7929
+ throw new ModelCliCommandError("MCP bridge URL is not a loopback URL this node produced");
7930
+ }
7931
+ return url;
7932
+ }
7933
+ __name(assertLoopbackUrl, "assertLoopbackUrl");
7934
+ var MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
7935
+ var MCP_LOOPBACK_URL_PATTERN = /^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp\/[a-f0-9]{8,64}$/;
7936
+ function assertModelId(model) {
7937
+ if (model === void 0) return null;
7938
+ if (!FLEET_AGENT_EXECUTION_MODEL_PATTERN.test(model)) {
7939
+ throw new ModelCliCommandError("Model id is not an opaque identifier");
7940
+ }
7941
+ return model;
7942
+ }
7943
+ __name(assertModelId, "assertModelId");
7944
+ var MODEL_CLI_OPTIONAL_FLAGS = {
7945
+ "claude-code": ["--effort", "--max-budget-usd"],
7946
+ codex: []
7947
+ };
7948
+ function unsupportedOptionalModelCliFlags(execution, supportedFlags) {
7949
+ if (!supportedFlags) return [];
7950
+ const wanted = [];
7951
+ if (execution.provider === "claude-code") {
7952
+ if (execution.effort) wanted.push("--effort");
7953
+ if (execution.maxBudgetUsd !== void 0) wanted.push("--max-budget-usd");
7954
+ }
7955
+ const droppable = new Set(
7956
+ MODEL_CLI_OPTIONAL_FLAGS[execution.provider] ?? []
7957
+ );
7958
+ return wanted.filter((flag) => droppable.has(flag) && !supportedFlags.has(flag));
7959
+ }
7960
+ __name(unsupportedOptionalModelCliFlags, "unsupportedOptionalModelCliFlags");
7961
+ function assertResumableSession(provider, sessionId) {
7962
+ if (sessionId === void 0) return null;
7963
+ if (!fleetAgentExecutionProviderSupportsSessionResume(provider)) {
7964
+ throw new ModelCliCommandError(`Provider '${String(provider)}' cannot resume a CLI session on the fleet`);
7965
+ }
7966
+ if (!isFleetAgentModelSessionId(sessionId)) {
7967
+ throw new ModelCliCommandError("CLI session id is not an opaque identifier");
7968
+ }
7969
+ return sessionId;
7970
+ }
7971
+ __name(assertResumableSession, "assertResumableSession");
7972
+ function formatBudget(value) {
7973
+ if (value === void 0) return null;
7974
+ if (!Number.isFinite(value) || value <= 0) {
7975
+ throw new ModelCliCommandError("maxBudgetUsd must be a positive number");
7976
+ }
7977
+ return String(Math.round(value * 100) / 100);
7978
+ }
7979
+ __name(formatBudget, "formatBudget");
7980
+ function buildModelCliCommand(input) {
7501
7981
  const platform = input.platform ?? process.platform;
7502
7982
  const { execution } = input;
7503
7983
  const resumeSessionId = assertResumableSession(execution.provider, input.resumeSessionId);
@@ -7553,7 +8033,21 @@ function buildModelCliCommand(input) {
7553
8033
  if (roots.length > 0) args.push("--add-dir", ...roots);
7554
8034
  } else if (execution.provider === "codex") {
7555
8035
  const sandbox = permissionMode === "plan" ? "read-only" : "workspace-write";
7556
- args.push("exec", "--json", "--sandbox", sandbox, "-C", quoteShellPath(input.workspacePath, platform));
8036
+ args.push(
8037
+ "exec",
8038
+ "--ignore-user-config",
8039
+ "--json",
8040
+ "--sandbox",
8041
+ sandbox,
8042
+ "-C",
8043
+ quoteShellPath(input.workspacePath, platform)
8044
+ );
8045
+ const kept = input.codexUserSettings ?? {};
8046
+ const windowsSandbox = assertCodexSetting("windows.sandbox", kept.windowsSandbox);
8047
+ const reasoningEffort = assertCodexSetting("model_reasoning_effort", kept.reasoningEffort);
8048
+ const ownerModel = assertCodexSetting("model", kept.model);
8049
+ if (windowsSandbox) args.push("-c", `windows.sandbox=${windowsSandbox}`);
8050
+ if (reasoningEffort) args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
7557
8051
  if (sandbox === "workspace-write") {
7558
8052
  for (const root of grantedRoots(writableMounts)) args.push("--add-dir", root);
7559
8053
  }
@@ -7563,7 +8057,8 @@ function buildModelCliCommand(input) {
7563
8057
  `mcp_servers.${assertMcpServerName(mcp.serverName)}.url=${assertLoopbackUrl(mcp.serverUrl)}`
7564
8058
  );
7565
8059
  }
7566
- if (model) args.push("-m", model);
8060
+ const codexModel = model ?? ownerModel;
8061
+ if (codexModel) args.push("-m", codexModel);
7567
8062
  if (execution.skipPermissions === true) args.push("--dangerously-bypass-approvals-and-sandbox");
7568
8063
  args.push("-");
7569
8064
  } else {
@@ -7586,7 +8081,16 @@ function assertMountGrantsInCommand(input) {
7586
8081
  }
7587
8082
  __name(assertMountGrantsInCommand, "assertMountGrantsInCommand");
7588
8083
  var STRICT_MCP_CONFIG_IN_COMMAND = / --mcp-config "[^"]+" --strict-mcp-config /;
8084
+ var CODEX_IGNORE_USER_CONFIG_IN_COMMAND = / exec --ignore-user-config /;
7589
8085
  function assertMcpIsolationInCommand(input) {
8086
+ if (input.execution.provider === "codex") {
8087
+ if (!CODEX_IGNORE_USER_CONFIG_IN_COMMAND.test(input.command)) {
8088
+ throw new ModelCliCommandError(
8089
+ "The codex command line does not carry exec --ignore-user-config, so the run would load the machine owner's own config.toml: its MCP servers, plugins and notify hook"
8090
+ );
8091
+ }
8092
+ return;
8093
+ }
7590
8094
  if (input.execution.provider !== "claude-code") return;
7591
8095
  if (!STRICT_MCP_CONFIG_IN_COMMAND.test(input.command)) {
7592
8096
  throw new ModelCliCommandError(
@@ -7914,424 +8418,183 @@ var MODEL_CLI_REQUIRED_FLAGS = {
7914
8418
  // bridge's. A CLI without them cannot run fleet work at all, so `doctor`
7915
8419
  // must say INCOMPATIBLE, not "runs that need them will fail".
7916
8420
  "claude-code": ["-p", "--output-format", "--permission-mode", "--mcp-config", "--strict-mcp-config"],
7917
- codex: ["--json", "--sandbox", "-C"]
8421
+ codex: ["--json", "--sandbox", "-C", "--ignore-user-config"]
7918
8422
  };
7919
8423
  var MODEL_CLI_CONDITIONAL_FLAGS = {
7920
- "claude-code": ["--model", "--add-dir", "--allowedTools", "--dangerously-skip-permissions"],
7921
- codex: ["--add-dir", "-c", "-m", "--dangerously-bypass-approvals-and-sandbox"]
7922
- };
7923
- var MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS = 10 * 6e4;
7924
- var MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS = 6e4;
7925
- var ModelCliCompatibilityProbe = class {
7926
- constructor(options) {
7927
- this.options = options;
7928
- this.now = options.now ?? (() => Date.now());
7929
- this.unstampedTtlMs = options.unstampedTtlMs ?? MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS;
7930
- this.incompleteRetryMs = options.incompleteRetryMs ?? MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS;
7931
- }
7932
- options;
7933
- static {
7934
- __name(this, "ModelCliCompatibilityProbe");
7935
- }
7936
- cache = /* @__PURE__ */ new Map();
7937
- inFlight = /* @__PURE__ */ new Map();
7938
- now;
7939
- unstampedTtlMs;
7940
- incompleteRetryMs;
7941
- /** One pinned binary. */
7942
- async probe(provider, executable) {
7943
- const stamp = await this.stamp(executable);
7944
- const key = `${provider}|${executable}|${stamp ? `${stamp.mtimeMs}:${stamp.size}` : "unstamped"}`;
7945
- const cached = this.cache.get(`${provider}|${executable}`);
7946
- if (cached && cached.key === key && (cached.permanent || this.now() - cached.probedAt < cached.ttlMs)) {
7947
- return cached.result;
7948
- }
7949
- const pending = this.inFlight.get(key);
7950
- if (pending) return pending;
7951
- const run = this.run(provider, executable).then(({ result, answered }) => {
7952
- this.cache.set(`${provider}|${executable}`, {
7953
- key,
7954
- // Only a COMPLETE answer from a stamped file is kept until the
7955
- // file changes. An incomplete one is retried soon, and an
7956
- // unstamped one on the slower clock.
7957
- permanent: answered && stamp !== null,
7958
- ttlMs: answered ? this.unstampedTtlMs : this.incompleteRetryMs,
7959
- probedAt: this.now(),
7960
- result
7961
- });
7962
- return result;
7963
- }).finally(() => {
7964
- this.inFlight.delete(key);
7965
- });
7966
- this.inFlight.set(key, run);
7967
- return run;
7968
- }
7969
- /** Every pinned binary in `paths`, in provider order. Unpinned providers are skipped. */
7970
- async probeAll(paths) {
7971
- const out = [];
7972
- for (const provider of MODEL_CLI_PROVIDERS) {
7973
- const executable = paths?.[provider];
7974
- if (typeof executable !== "string" || !executable.trim()) continue;
7975
- out.push(await this.probe(provider, executable));
7976
- }
7977
- return out;
7978
- }
7979
- async stamp(executable) {
7980
- if (!this.options.statFile) return null;
7981
- try {
7982
- const stamp = await this.options.statFile(executable);
7983
- return stamp && Number.isFinite(stamp.mtimeMs) && Number.isFinite(stamp.size) ? stamp : null;
7984
- } catch {
7985
- return null;
7986
- }
7987
- }
7988
- async run(provider, executable) {
7989
- const command = this.commandFor(executable);
7990
- const versionOutput = await this.ask(command, ["--version"]);
7991
- const version = versionOutput === null ? null : parseCliVersion(versionOutput);
7992
- const help = await this.ask(command, [...HELP_ARGS[provider]]);
7993
- return {
7994
- result: judgeModelCliHelp(provider, executable, version, help),
7995
- // "Answered" = both invocations produced output. An UNRECOGNISED
7996
- // help text is still an answer — deterministic for that file — and
7997
- // re-asking every minute would learn nothing.
7998
- answered: versionOutput !== null && help !== null
7999
- };
8000
- }
8001
- /**
8002
- * The runner on Windows goes through `cmd.exe` (`shell: true`), which
8003
- * splits an unquoted `C:\Program Files\…` at the space.
8004
- */
8005
- commandFor(executable) {
8006
- return this.options.platform === "win32" && /\s/.test(executable) && !executable.startsWith('"') ? `"${executable}"` : executable;
8007
- }
8008
- /** stdout+stderr of one invocation, or null when it could not be run at all. */
8009
- async ask(command, args) {
8010
- try {
8011
- const result = await this.options.runner.run(command, args);
8012
- if (result.timedOut) return null;
8013
- const text = `${result.stdout ?? ""}
8014
- ${result.stderr ?? ""}`;
8015
- return result.code === 0 || text.trim() ? text : null;
8016
- } catch {
8017
- return null;
8018
- }
8019
- }
8020
- };
8021
- function parseAdvertisedFlags(help) {
8022
- const flags = /* @__PURE__ */ new Set();
8023
- const pattern = /(?:^|[\s,[(|])(--?[A-Za-z][A-Za-z0-9-]*)(?=$|[\s,=<[\])|.:])/gm;
8024
- for (const match of help.matchAll(pattern)) {
8025
- flags.add(match[1]);
8026
- }
8027
- return flags;
8028
- }
8029
- __name(parseAdvertisedFlags, "parseAdvertisedFlags");
8030
- function judgeModelCliHelp(provider, executable, version, help) {
8031
- const required = MODEL_CLI_REQUIRED_FLAGS[provider];
8032
- const base = {
8033
- provider,
8034
- executable,
8035
- version,
8036
- supportedFlags: null,
8037
- missingRequiredFlags: [],
8038
- missingConditionalFlags: [],
8039
- unsupportedOptionalFlags: [],
8040
- note: null
8041
- };
8042
- if (help === null) {
8043
- return { ...base, note: "the binary did not answer --help; nothing is dropped" };
8044
- }
8045
- const advertised = parseAdvertisedFlags(help);
8046
- if (!required.some((flag) => advertised.has(flag))) {
8047
- return {
8048
- ...base,
8049
- note: `the help text lists none of the flags this node always passes (${required.join(", ")}); its format is not recognised, so nothing is dropped`
8050
- };
8051
- }
8052
- const missing = /* @__PURE__ */ __name((list) => list.filter((flag) => !advertised.has(flag)), "missing");
8053
- return {
8054
- ...base,
8055
- supportedFlags: advertised,
8056
- missingRequiredFlags: missing(required),
8057
- missingConditionalFlags: missing(MODEL_CLI_CONDITIONAL_FLAGS[provider]),
8058
- unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider])
8059
- };
8060
- }
8061
- __name(judgeModelCliHelp, "judgeModelCliHelp");
8062
- function isModelCliCompatible(compat) {
8063
- return compat.missingRequiredFlags.length === 0;
8064
- }
8065
- __name(isModelCliCompatible, "isModelCliCompatible");
8066
- function describeCliVersions(results) {
8067
- return results.map((result) => `${result.provider} ${result.version ?? "unknown"}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH)).slice(0, FLEET_MAX_CLI_VERSIONS);
8068
- }
8069
- __name(describeCliVersions, "describeCliVersions");
8070
- var LEGACY_COMMAND_NAME = {
8071
- "claude-code": "claude",
8072
- codex: "codex"
8073
- };
8074
- function primaryPinnedCliVersion(results) {
8075
- const first = results.find((result) => result.version !== null);
8076
- return first ? `${LEGACY_COMMAND_NAME[first.provider]} ${first.version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH) : null;
8077
- }
8078
- __name(primaryPinnedCliVersion, "primaryPinnedCliVersion");
8079
-
8080
- // src/core/worker-loop.ts
8081
- var import_node_perf_hooks = require("node:perf_hooks");
8082
-
8083
- // src/core/heartbeat.ts
8084
- var systemScheduler = {
8085
- setTimeout: /* @__PURE__ */ __name((callback, ms) => setTimeout(callback, ms), "setTimeout"),
8086
- clearTimeout: /* @__PURE__ */ __name((handle) => clearTimeout(handle), "clearTimeout")
8087
- };
8088
- var OPTIONAL_DESCRIPTION_FIELD_TIERS = [
8089
- // NEWEST first. Node lifecycle + remote node limits (self-build slices
8090
- // AR, AS). Tiered (review): `cliVersions` goes out on EVERY beat of a
8091
- // node with a pinned CLI, so an upgraded node talking to a platform that
8092
- // predates it would otherwise drop the worker state and housekeeping
8093
- // along with it — losing health signals that platform DOES understand,
8094
- // for the life of the process. Stripping the newest tier first keeps them.
8095
- ["cliVersions", "maxConcurrentJobs", "maxCpuPercent", "maxMemoryMb"],
8096
- // Fleet health signals (EW-776) and node housekeeping (EW-803).
8097
- [
8098
- "workerState",
8099
- "workerStateReason",
8100
- "minFreeDiskBytes",
8101
- "workspaceCount",
8102
- "workspaceBytes",
8103
- "lastReclaimAt",
8104
- "lastReclaimFreedBytes"
8105
- ]
8106
- ];
8107
- function computeBackoffDelay(intervalMs, failures, maxMs) {
8108
- if (failures <= 0) {
8109
- return intervalMs;
8110
- }
8111
- const exponent = Math.min(failures, 30);
8112
- const delay2 = intervalMs * 2 ** exponent;
8113
- return Math.min(delay2, maxMs);
8114
- }
8115
- __name(computeBackoffDelay, "computeBackoffDelay");
8116
- var HeartbeatLoop = class {
8117
- static {
8118
- __name(this, "HeartbeatLoop");
8119
- }
8120
- options;
8121
- scheduler;
8122
- now;
8123
- listeners = /* @__PURE__ */ new Set();
8124
- timer = null;
8125
- running = false;
8126
- inFlight = null;
8127
- /**
8128
- * How many optional-field TIERS (newest first) this platform has proven
8129
- * it predates; from then on the beat leaves those tiers out. 0 = it
8130
- * takes everything. Process-scoped on purpose — a platform upgrade is a
8131
- * restart-shaped event for the node, and re-probing on every beat would
8132
- * mean one wasted round trip per beat, forever, against an API that
8133
- * will never accept them.
8134
- */
8135
- strippedTiers = 0;
8136
- state = {
8137
- state: "idle",
8138
- lastHeartbeatAt: null,
8139
- consecutiveFailures: 0,
8140
- nextAttemptInMs: null,
8141
- lastError: null,
8142
- lastErrorKind: null,
8143
- node: null
8144
- };
8145
- constructor(options) {
8146
- this.options = {
8147
- ...options,
8148
- intervalMs: options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
8149
- maxBackoffMs: options.maxBackoffMs ?? MAX_HEARTBEAT_BACKOFF_MS
8150
- };
8151
- this.scheduler = options.scheduler ?? systemScheduler;
8152
- this.now = options.now ?? (() => Date.now());
8153
- this.options.logger?.protect(options.secret);
8154
- }
8155
- getState() {
8156
- return { ...this.state };
8157
- }
8158
- onChange(listener) {
8159
- this.listeners.add(listener);
8160
- return () => {
8161
- this.listeners.delete(listener);
8162
- };
8163
- }
8164
- /** Start beating. Resolves once the FIRST beat has settled. */
8165
- start() {
8166
- if (this.running) {
8167
- return this.inFlight ?? Promise.resolve();
8168
- }
8169
- this.running = true;
8170
- return this.tick();
8171
- }
8172
- /** Stop beating and cancel any pending timer. */
8173
- stop() {
8174
- this.running = false;
8175
- this.cancelTimer();
8176
- this.patch({ state: "stopped", nextAttemptInMs: null });
8177
- }
8178
- /** Await the in-flight beat, if any (test/shutdown helper). */
8179
- async settled() {
8180
- await this.inFlight;
8181
- }
8182
- /** Run one beat now. Exposed so the UI can offer "retry" and tests can step. */
8183
- tick() {
8184
- const run = this.runOnce().finally(() => {
8185
- if (this.inFlight === run) {
8186
- this.inFlight = null;
8187
- }
8188
- });
8189
- this.inFlight = run;
8190
- return run;
8191
- }
8192
- async runOnce() {
8193
- if (!this.running) {
8194
- return;
8195
- }
8196
- this.cancelTimer();
8197
- if (this.state.state !== "connected") {
8198
- this.patch({ state: "connecting" });
8199
- }
8200
- try {
8201
- const description = await this.options.describe();
8202
- this.onSuccess(await this.beat(description));
8203
- } catch (error) {
8204
- this.onFailure(error);
8205
- }
8206
- this.scheduleNext();
8207
- }
8208
- /**
8209
- * Send one beat, tolerating a platform that predates the optional
8210
- * self-description fields (the worker state, EW-776; the housekeeping
8211
- * report, EW-803).
8212
- *
8213
- * The API validates heartbeats with `whitelist + forbidNonWhitelisted`,
8214
- * so a field an older build does not know is not ignored — it 400s the
8215
- * whole request. A 400 is a FAILED beat, a failed beat backs off, and
8216
- * enough of them sweep the node to `offline`. Which would mean shipping
8217
- * a health-reporting feature whose failure mode is "every node in a
8218
- * mixed-version fleet goes dark": the exact class of outage this slice
8219
- * was written to end.
8220
- *
8221
- * So a 400 on a beat that CARRIED those fields is retried once,
8222
- * immediately, without them. If that succeeds the platform is simply
8223
- * older than this daemon; we latch, say so once, and keep reporting
8224
- * liveness. The retry costs one request, once per process.
8225
- *
8226
- * Inferring "the field was rejected" from a bare 400 is deliberate and
8227
- * has a cost worth stating: `FleetClient` never surfaces server bodies
8228
- * (fixed client-authored messages only), so a 400 caused by something
8229
- * else on such a beat also latches, and this node reports liveness only
8230
- * until it restarts. That is the safe direction — the node stays
8231
- * observable either way, and only the extra signal is lost.
8232
- */
8233
- async beat(description) {
8234
- const credential = { nodeId: this.options.nodeId, secret: this.options.secret };
8235
- let sent = stripTiers(description, this.strippedTiers);
8236
- try {
8237
- return await this.options.client.heartbeat({ ...credential, ...sent });
8238
- } catch (error) {
8239
- if (!isFieldRejection(error)) throw error;
8240
- let lastError = error;
8241
- for (let tiers = this.strippedTiers + 1; tiers <= OPTIONAL_DESCRIPTION_FIELD_TIERS.length; tiers += 1) {
8242
- const candidate = stripTiers(description, tiers);
8243
- if (Object.keys(candidate).length === Object.keys(sent).length) continue;
8244
- sent = candidate;
8245
- try {
8246
- const result = await this.options.client.heartbeat({ ...credential, ...candidate });
8247
- this.strippedTiers = tiers;
8248
- this.options.logger?.warn(
8249
- tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length ? "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts." : "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts."
8250
- );
8251
- return result;
8252
- } catch (retryError) {
8253
- if (!isFieldRejection(retryError)) throw retryError;
8254
- lastError = retryError;
8255
- }
8256
- }
8257
- throw lastError;
8258
- }
8424
+ "claude-code": [
8425
+ "--model",
8426
+ "--add-dir",
8427
+ "--allowedTools",
8428
+ "--dangerously-skip-permissions",
8429
+ "--verbose",
8430
+ "--resume",
8431
+ "--fork-session"
8432
+ ],
8433
+ codex: ["--add-dir", "-c", "-m", "--dangerously-bypass-approvals-and-sandbox"]
8434
+ };
8435
+ var MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS = 10 * 6e4;
8436
+ var MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS = 60 * 6e4;
8437
+ var MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS = 6e4;
8438
+ var ModelCliCompatibilityProbe = class {
8439
+ constructor(options) {
8440
+ this.options = options;
8441
+ this.now = options.now ?? (() => Date.now());
8442
+ this.unstampedTtlMs = options.unstampedTtlMs ?? MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS;
8443
+ this.incompleteRetryMs = options.incompleteRetryMs ?? MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS;
8444
+ this.stampedMaxAgeMs = options.stampedMaxAgeMs ?? MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS;
8259
8445
  }
8260
- onSuccess(result) {
8261
- this.patch({
8262
- state: "connected",
8263
- lastHeartbeatAt: this.now(),
8264
- consecutiveFailures: 0,
8265
- lastError: null,
8266
- lastErrorKind: null,
8267
- node: result.node
8268
- });
8269
- if (this.options.onAccepted) {
8270
- try {
8271
- this.options.onAccepted(result);
8272
- } catch {
8273
- }
8274
- }
8446
+ options;
8447
+ static {
8448
+ __name(this, "ModelCliCompatibilityProbe");
8275
8449
  }
8276
- onFailure(error) {
8277
- const kind = error instanceof FleetClientError ? error.kind : "network";
8278
- const raw = error instanceof Error ? error.message : String(error);
8279
- const message = this.options.logger?.redact(raw) ?? raw;
8280
- const failures = this.state.consecutiveFailures + 1;
8281
- this.patch({
8282
- // `unauthorized` is sticky and visible: the operator has to act
8283
- // (re-enable the node in Fleet, or re-enroll). We keep retrying at
8284
- // the backoff ceiling so a re-enabled node recovers on its own.
8285
- state: kind === "unauthorized" ? "unauthorized" : "retrying",
8286
- consecutiveFailures: failures,
8287
- lastError: message,
8288
- lastErrorKind: kind
8450
+ cache = /* @__PURE__ */ new Map();
8451
+ inFlight = /* @__PURE__ */ new Map();
8452
+ now;
8453
+ unstampedTtlMs;
8454
+ incompleteRetryMs;
8455
+ stampedMaxAgeMs;
8456
+ /** One pinned binary. */
8457
+ async probe(provider, executable) {
8458
+ const stamp = await this.stamp(executable);
8459
+ const key = `${provider}|${executable}|${stamp ? `${stamp.mtimeMs}:${stamp.size}` : "unstamped"}`;
8460
+ const cached = this.cache.get(`${provider}|${executable}`);
8461
+ if (cached && cached.key === key && this.now() - cached.probedAt < cached.ttlMs) {
8462
+ return cached.result;
8463
+ }
8464
+ const pending = this.inFlight.get(key);
8465
+ if (pending) return pending;
8466
+ const run = this.run(provider, executable).then(({ result, answered }) => {
8467
+ this.cache.set(`${provider}|${executable}`, {
8468
+ key,
8469
+ // A COMPLETE answer from a stamped file is kept until the file
8470
+ // changes or, at the latest, for `stampedMaxAgeMs` (an upgrade
8471
+ // behind a stable wrapper does not change the stamp). An
8472
+ // incomplete one is retried soon, and an unstamped one on the
8473
+ // ten-minute clock.
8474
+ ttlMs: !answered ? this.incompleteRetryMs : stamp !== null ? this.stampedMaxAgeMs : this.unstampedTtlMs,
8475
+ probedAt: this.now(),
8476
+ result
8477
+ });
8478
+ return result;
8479
+ }).finally(() => {
8480
+ this.inFlight.delete(key);
8289
8481
  });
8290
- this.options.logger?.warn(`Heartbeat failed (attempt ${failures}): ${message}`);
8482
+ this.inFlight.set(key, run);
8483
+ return run;
8291
8484
  }
8292
- scheduleNext() {
8293
- if (!this.running) {
8294
- return;
8485
+ /** Every pinned binary in `paths`, in provider order. Unpinned providers are skipped. */
8486
+ async probeAll(paths) {
8487
+ const out = [];
8488
+ for (const provider of MODEL_CLI_PROVIDERS) {
8489
+ const executable = paths?.[provider];
8490
+ if (typeof executable !== "string" || !executable.trim()) continue;
8491
+ out.push(await this.probe(provider, executable));
8295
8492
  }
8296
- const delay2 = computeBackoffDelay(
8297
- this.options.intervalMs,
8298
- this.state.consecutiveFailures,
8299
- this.options.maxBackoffMs
8300
- );
8301
- this.patch({ nextAttemptInMs: delay2 });
8302
- this.timer = this.scheduler.setTimeout(() => {
8303
- this.timer = null;
8304
- void this.tick();
8305
- }, delay2);
8493
+ return out;
8306
8494
  }
8307
- cancelTimer() {
8308
- if (this.timer !== null) {
8309
- this.scheduler.clearTimeout(this.timer);
8310
- this.timer = null;
8495
+ async stamp(executable) {
8496
+ if (!this.options.statFile) return null;
8497
+ try {
8498
+ const stamp = await this.options.statFile(executable);
8499
+ return stamp && Number.isFinite(stamp.mtimeMs) && Number.isFinite(stamp.size) ? stamp : null;
8500
+ } catch {
8501
+ return null;
8311
8502
  }
8312
8503
  }
8313
- patch(next) {
8314
- this.state = { ...this.state, ...next };
8315
- const snapshot = this.getState();
8316
- for (const listener of this.listeners) {
8317
- listener(snapshot);
8504
+ async run(provider, executable) {
8505
+ const command = this.commandFor(executable);
8506
+ const versionOutput = await this.ask(command, ["--version"]);
8507
+ const version = versionOutput === null ? null : parseCliVersion(versionOutput);
8508
+ const help = await this.ask(command, [...HELP_ARGS[provider]]);
8509
+ return {
8510
+ result: judgeModelCliHelp(provider, executable, version, help),
8511
+ // "Answered" = both invocations produced output. An UNRECOGNISED
8512
+ // help text is still an answer — deterministic for that file — and
8513
+ // re-asking every minute would learn nothing.
8514
+ answered: versionOutput !== null && help !== null
8515
+ };
8516
+ }
8517
+ /**
8518
+ * The runner on Windows goes through `cmd.exe` (`shell: true`), which
8519
+ * splits an unquoted `C:\Program Files\…` at the space.
8520
+ */
8521
+ commandFor(executable) {
8522
+ return this.options.platform === "win32" && /\s/.test(executable) && !executable.startsWith('"') ? `"${executable}"` : executable;
8523
+ }
8524
+ /** stdout+stderr of one invocation, or null when it could not be run at all. */
8525
+ async ask(command, args) {
8526
+ try {
8527
+ const result = await this.options.runner.run(command, args);
8528
+ if (result.timedOut) return null;
8529
+ const text = `${result.stdout ?? ""}
8530
+ ${result.stderr ?? ""}`;
8531
+ return result.code === 0 || text.trim() ? text : null;
8532
+ } catch {
8533
+ return null;
8318
8534
  }
8319
8535
  }
8320
8536
  };
8321
- function stripTiers(description, tiers) {
8322
- const out = {};
8323
- const dropped = new Set(OPTIONAL_DESCRIPTION_FIELD_TIERS.slice(0, tiers).flat());
8324
- for (const [key, value] of Object.entries(description)) {
8325
- if (value === void 0 || dropped.has(key)) continue;
8326
- out[key] = value;
8537
+ function parseAdvertisedFlags(help) {
8538
+ const flags = /* @__PURE__ */ new Set();
8539
+ const pattern = /(?:^|[\s,[(|])(--?[A-Za-z][A-Za-z0-9-]*)(?=$|[\s,=<[\])|.:])/gm;
8540
+ for (const match of help.matchAll(pattern)) {
8541
+ flags.add(match[1]);
8327
8542
  }
8328
- return out;
8543
+ return flags;
8329
8544
  }
8330
- __name(stripTiers, "stripTiers");
8331
- function isFieldRejection(error) {
8332
- return error instanceof FleetClientError && error.kind === "invalid-request" && error.status === 400;
8545
+ __name(parseAdvertisedFlags, "parseAdvertisedFlags");
8546
+ function judgeModelCliHelp(provider, executable, version, help) {
8547
+ const required = MODEL_CLI_REQUIRED_FLAGS[provider];
8548
+ const base = {
8549
+ provider,
8550
+ executable,
8551
+ version,
8552
+ supportedFlags: null,
8553
+ missingRequiredFlags: [],
8554
+ missingConditionalFlags: [],
8555
+ unsupportedOptionalFlags: [],
8556
+ note: null
8557
+ };
8558
+ if (help === null) {
8559
+ return { ...base, note: "the binary did not answer --help; nothing is dropped" };
8560
+ }
8561
+ const advertised = parseAdvertisedFlags(help);
8562
+ if (!required.some((flag) => advertised.has(flag))) {
8563
+ return {
8564
+ ...base,
8565
+ note: `the help text lists none of the flags this node always passes (${required.join(", ")}); its format is not recognised, so nothing is dropped`
8566
+ };
8567
+ }
8568
+ const missing = /* @__PURE__ */ __name((list) => list.filter((flag) => !advertised.has(flag)), "missing");
8569
+ return {
8570
+ ...base,
8571
+ supportedFlags: advertised,
8572
+ missingRequiredFlags: missing(required),
8573
+ missingConditionalFlags: missing(MODEL_CLI_CONDITIONAL_FLAGS[provider]),
8574
+ unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider])
8575
+ };
8333
8576
  }
8334
- __name(isFieldRejection, "isFieldRejection");
8577
+ __name(judgeModelCliHelp, "judgeModelCliHelp");
8578
+ function isModelCliCompatible(compat) {
8579
+ return compat.missingRequiredFlags.length === 0;
8580
+ }
8581
+ __name(isModelCliCompatible, "isModelCliCompatible");
8582
+ function describeCliVersions(results) {
8583
+ return results.map((result) => `${result.provider} ${result.version ?? "unknown"}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH)).slice(0, FLEET_MAX_CLI_VERSIONS);
8584
+ }
8585
+ __name(describeCliVersions, "describeCliVersions");
8586
+ var LEGACY_COMMAND_NAME = {
8587
+ "claude-code": "claude",
8588
+ codex: "codex"
8589
+ };
8590
+ function primaryPinnedCliVersion(results) {
8591
+ const first = results.find((result) => result.version !== null);
8592
+ return first ? `${LEGACY_COMMAND_NAME[first.provider]} ${first.version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH) : null;
8593
+ }
8594
+ __name(primaryPinnedCliVersion, "primaryPinnedCliVersion");
8595
+
8596
+ // src/core/worker-loop.ts
8597
+ var import_node_perf_hooks = require("node:perf_hooks");
8335
8598
 
8336
8599
  // src/core/resource-limits.ts
8337
8600
  var ADMIT = { admit: true, reason: null };
@@ -12945,6 +13208,11 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
12945
13208
  emptyMcpConfigPath = mcpConfigPath;
12946
13209
  }
12947
13210
  const droppedFlags = await optionalFlagsToDrop(jobId, execution, executable, io);
13211
+ const codexUserSettings = execution.provider === "codex" ? await readOwnerCodexSettings(
13212
+ jobId,
13213
+ io,
13214
+ effectiveCodexHome(io, modelEnvOverlay, execution.envPassthrough, execution.envGrants)
13215
+ ) : void 0;
12948
13216
  const invoke = /* @__PURE__ */ __name(async (instructions, resumeSessionId) => {
12949
13217
  await scratchFs.writeFile(scratch.instructionsPath, instructions);
12950
13218
  let command;
@@ -12957,6 +13225,7 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
12957
13225
  ...mounts && mounts.length > 0 ? { mounts } : {},
12958
13226
  ...bridge.cli ? { mcp: bridge.cli } : {},
12959
13227
  ...emptyMcpConfigPath ? { emptyMcpConfigPath } : {},
13228
+ ...codexUserSettings ? { codexUserSettings } : {},
12960
13229
  ...resumeSessionId ? { resumeSessionId } : {},
12961
13230
  ...io.platform ? { platform: io.platform } : {},
12962
13231
  ...streamClaude ? { stream: true } : {},
@@ -13175,6 +13444,32 @@ function resolveRealHomeDir(io) {
13175
13444
  return (0, import_os2.homedir)();
13176
13445
  }
13177
13446
  __name(resolveRealHomeDir, "resolveRealHomeDir");
13447
+ async function readOwnerCodexSettings(jobId, io, codexHome) {
13448
+ const read = io.sessionConfigFs?.readFile?.bind(io.sessionConfigFs);
13449
+ if (!read) return {};
13450
+ try {
13451
+ return readCodexUserSettings(await read((0, import_path4.join)(codexHome, "config.toml")));
13452
+ } catch (error) {
13453
+ io.logger?.warn(
13454
+ `[fleet-node] job ${jobId}: could not read the Codex config to keep its sandbox and model defaults; the run uses Codex's own defaults (still under --ignore-user-config): ${describeContainmentError(error)}`
13455
+ );
13456
+ return {};
13457
+ }
13458
+ }
13459
+ __name(readOwnerCodexSettings, "readOwnerCodexSettings");
13460
+ function effectiveCodexHome(io, envOverlay, envPassthrough, envGrants) {
13461
+ const isCodexHome = /* @__PURE__ */ __name((name) => name.toUpperCase() === "CODEX_HOME", "isCodexHome");
13462
+ const childEnv = buildNodeCheckEnv(
13463
+ envPassthrough?.filter(isCodexHome) ?? null,
13464
+ io.parentEnv,
13465
+ envGrants?.filter(isCodexHome) ?? null,
13466
+ envOverlay
13467
+ );
13468
+ const key = Object.keys(childEnv).find(isCodexHome);
13469
+ const value = key === void 0 ? void 0 : childEnv[key];
13470
+ return typeof value === "string" && value.trim() ? value.trim() : (0, import_path4.join)(resolveRealHomeDir(io), ".codex");
13471
+ }
13472
+ __name(effectiveCodexHome, "effectiveCodexHome");
13178
13473
  function describeContainmentError(error) {
13179
13474
  return error instanceof Error ? error.message : String(error);
13180
13475
  }
@@ -14986,10 +15281,14 @@ var USER_SELECTABLE_WORK_KINDS2 = [
14986
15281
  "repo",
14987
15282
  "app"
14988
15283
  ];
14989
- var WORK_KINDS2 = [...USER_SELECTABLE_WORK_KINDS2, "company", "campaign", "default"];
15284
+ var WORK_KINDS2 = [...USER_SELECTABLE_WORK_KINDS2, "company", "campaign", "project", "default"];
14990
15285
  new Set(WORK_KINDS2);
14991
15286
  var GOAL_KINDS2 = ["metric", "delivery"];
14992
15287
  new Set(GOAL_KINDS2);
15288
+ var RECORD_VISIBILITIES2 = ["private", "team", "organization"];
15289
+ new Set(RECORD_VISIBILITIES2);
15290
+ var RECORD_LIST_VIEWS2 = ["mine", "shared", "organization"];
15291
+ new Set(RECORD_LIST_VIEWS2);
14993
15292
  var VERSION2 = "1.0.0";
14994
15293
  var GTM_SKILLS2 = [
14995
15294
  // ── research ────────────────────────────────────────────────────────
@@ -15695,6 +15994,113 @@ var ACTION_CATEGORIES2 = Object.freeze([
15695
15994
  Object.freeze(
15696
15995
  ACTION_CATEGORIES2.filter((c) => c !== "read.internal")
15697
15996
  );
15997
+ var DOMAIN_EVENT_CATALOG2 = [
15998
+ { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
15999
+ {
16000
+ type: "organization.member.added",
16001
+ domain: "organization",
16002
+ aggregateType: "organization",
16003
+ phase: "P1",
16004
+ child: "member"
16005
+ },
16006
+ {
16007
+ type: "organization.member.updated",
16008
+ domain: "organization",
16009
+ aggregateType: "organization",
16010
+ phase: "P1",
16011
+ child: "member"
16012
+ },
16013
+ {
16014
+ type: "organization.member.removed",
16015
+ domain: "organization",
16016
+ aggregateType: "organization",
16017
+ phase: "P1",
16018
+ child: "member"
16019
+ },
16020
+ { type: "agent.created", domain: "agents", aggregateType: "agent", phase: "P1" },
16021
+ { type: "agent.updated", domain: "agents", aggregateType: "agent", phase: "P1" },
16022
+ { type: "agent.status_changed", domain: "agents", aggregateType: "agent", phase: "P1" },
16023
+ { type: "agent.archived", domain: "agents", aggregateType: "agent", phase: "P1" },
16024
+ { type: "agent.deleted", domain: "agents", aggregateType: "agent", phase: "P1" },
16025
+ { type: "team.created", domain: "teams", aggregateType: "team", phase: "P1" },
16026
+ { type: "team.updated", domain: "teams", aggregateType: "team", phase: "P1" },
16027
+ { type: "team.deleted", domain: "teams", aggregateType: "team", phase: "P1" },
16028
+ { type: "team.member.added", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
16029
+ { type: "team.member.updated", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
16030
+ { type: "team.member.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
16031
+ { type: "team.resource.added", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
16032
+ { type: "team.resource.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
16033
+ { type: "work.created", domain: "works", aggregateType: "work", phase: "P1" },
16034
+ { type: "work.updated", domain: "works", aggregateType: "work", phase: "P1" },
16035
+ { type: "work.archived", domain: "works", aggregateType: "work", phase: "P1" },
16036
+ { type: "work.deleted", domain: "works", aggregateType: "work", phase: "P1" },
16037
+ { type: "work.member.added", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
16038
+ { type: "work.member.updated", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
16039
+ { type: "work.member.removed", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
16040
+ { type: "mission.created", domain: "missions", aggregateType: "mission", phase: "P2" },
16041
+ { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
16042
+ { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
16043
+ { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
16044
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
16045
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
16046
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
16047
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
16048
+ { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
16049
+ { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
16050
+ { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
16051
+ { type: "goal.archived", domain: "goals", aggregateType: "goal", phase: "P2" },
16052
+ { type: "goal.unarchived", domain: "goals", aggregateType: "goal", phase: "P2" },
16053
+ { type: "goal.deleted", domain: "goals", aggregateType: "goal", phase: "P2" },
16054
+ { type: "goal.progress_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
16055
+ { type: "idea.created", domain: "ideas", aggregateType: "idea", phase: "P2" },
16056
+ { type: "idea.updated", domain: "ideas", aggregateType: "idea", phase: "P2" },
16057
+ { type: "idea.status_changed", domain: "ideas", aggregateType: "idea", phase: "P2" },
16058
+ { type: "idea.deleted", domain: "ideas", aggregateType: "idea", phase: "P2" },
16059
+ { type: "task.created", domain: "tasks", aggregateType: "task", phase: "P1" },
16060
+ { type: "task.updated", domain: "tasks", aggregateType: "task", phase: "P1" },
16061
+ { type: "task.transitioned", domain: "tasks", aggregateType: "task", phase: "P1" },
16062
+ { type: "task.deleted", domain: "tasks", aggregateType: "task", phase: "P1" },
16063
+ { type: "task.assignee.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
16064
+ { type: "task.assignee.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
16065
+ { type: "task.watcher.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
16066
+ { type: "task.watcher.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
16067
+ { type: "task.block.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
16068
+ { type: "task.block.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
16069
+ { type: "task.relation.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
16070
+ { type: "task.relation.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
16071
+ { type: "task.comment.created", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
16072
+ { type: "task.comment.updated", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
16073
+ { type: "task.comment.deleted", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
16074
+ { type: "task.attachment.added", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
16075
+ { type: "task.attachment.removed", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
16076
+ { type: "task.runtime_updated", domain: "tasks", aggregateType: "task", phase: "P1", optIn: true },
16077
+ { type: "inbox.item.created", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
16078
+ { type: "inbox.item.answered", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
16079
+ { type: "inbox.item.state_changed", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
16080
+ { type: "inbox.item.deleted", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
16081
+ { type: "kb.document.created", domain: "kb", aggregateType: "kb_document", phase: "P3" },
16082
+ { type: "kb.document.updated", domain: "kb", aggregateType: "kb_document", phase: "P3" },
16083
+ { type: "kb.document.moved", domain: "kb", aggregateType: "kb_document", phase: "P3" },
16084
+ { type: "kb.document.archived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
16085
+ { type: "kb.document.unarchived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
16086
+ { type: "kb.document.deleted", domain: "kb", aggregateType: "kb_document", phase: "P3" },
16087
+ { type: "kb.document.review_changed", domain: "kb", aggregateType: "kb_document", phase: "P3" },
16088
+ { type: "kb.folder.created", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
16089
+ { type: "kb.folder.updated", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
16090
+ { type: "kb.folder.deleted", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
16091
+ { type: "email.message.received", domain: "email", aggregateType: "email_message", phase: "P4" },
16092
+ { type: "email.message.sent", domain: "email", aggregateType: "email_message", phase: "P4" },
16093
+ { type: "email.message.status_changed", domain: "email", aggregateType: "email_message", phase: "P4" },
16094
+ { type: "task.visibility_changed", domain: "visibility", aggregateType: "task", phase: "P1" },
16095
+ { type: "agent.visibility_changed", domain: "visibility", aggregateType: "agent", phase: "P1" },
16096
+ { type: "goal.visibility_changed", domain: "visibility", aggregateType: "goal", phase: "P2" },
16097
+ { type: "mission.visibility_changed", domain: "visibility", aggregateType: "mission", phase: "P2" },
16098
+ { type: "idea.visibility_changed", domain: "visibility", aggregateType: "idea", phase: "P2" }
16099
+ ];
16100
+ DOMAIN_EVENT_CATALOG2.map((entry) => entry.type);
16101
+ new Map(
16102
+ DOMAIN_EVENT_CATALOG2.map((entry) => [entry.type, entry])
16103
+ );
15698
16104
  var WorkspaceNotProvisionedError = class extends Error {
15699
16105
  static {
15700
16106
  __name(this, "WorkspaceNotProvisionedError");
@@ -19193,10 +19599,14 @@ var USER_SELECTABLE_WORK_KINDS3 = [
19193
19599
  "repo",
19194
19600
  "app"
19195
19601
  ];
19196
- var WORK_KINDS3 = [...USER_SELECTABLE_WORK_KINDS3, "company", "campaign", "default"];
19602
+ var WORK_KINDS3 = [...USER_SELECTABLE_WORK_KINDS3, "company", "campaign", "project", "default"];
19197
19603
  new Set(WORK_KINDS3);
19198
19604
  var GOAL_KINDS3 = ["metric", "delivery"];
19199
19605
  new Set(GOAL_KINDS3);
19606
+ var RECORD_VISIBILITIES3 = ["private", "team", "organization"];
19607
+ new Set(RECORD_VISIBILITIES3);
19608
+ var RECORD_LIST_VIEWS3 = ["mine", "shared", "organization"];
19609
+ new Set(RECORD_LIST_VIEWS3);
19200
19610
  var VERSION3 = "1.0.0";
19201
19611
  var GTM_SKILLS3 = [
19202
19612
  // ── research ────────────────────────────────────────────────────────
@@ -19902,6 +20312,113 @@ var ACTION_CATEGORIES3 = Object.freeze([
19902
20312
  Object.freeze(
19903
20313
  ACTION_CATEGORIES3.filter((c) => c !== "read.internal")
19904
20314
  );
20315
+ var DOMAIN_EVENT_CATALOG3 = [
20316
+ { type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
20317
+ {
20318
+ type: "organization.member.added",
20319
+ domain: "organization",
20320
+ aggregateType: "organization",
20321
+ phase: "P1",
20322
+ child: "member"
20323
+ },
20324
+ {
20325
+ type: "organization.member.updated",
20326
+ domain: "organization",
20327
+ aggregateType: "organization",
20328
+ phase: "P1",
20329
+ child: "member"
20330
+ },
20331
+ {
20332
+ type: "organization.member.removed",
20333
+ domain: "organization",
20334
+ aggregateType: "organization",
20335
+ phase: "P1",
20336
+ child: "member"
20337
+ },
20338
+ { type: "agent.created", domain: "agents", aggregateType: "agent", phase: "P1" },
20339
+ { type: "agent.updated", domain: "agents", aggregateType: "agent", phase: "P1" },
20340
+ { type: "agent.status_changed", domain: "agents", aggregateType: "agent", phase: "P1" },
20341
+ { type: "agent.archived", domain: "agents", aggregateType: "agent", phase: "P1" },
20342
+ { type: "agent.deleted", domain: "agents", aggregateType: "agent", phase: "P1" },
20343
+ { type: "team.created", domain: "teams", aggregateType: "team", phase: "P1" },
20344
+ { type: "team.updated", domain: "teams", aggregateType: "team", phase: "P1" },
20345
+ { type: "team.deleted", domain: "teams", aggregateType: "team", phase: "P1" },
20346
+ { type: "team.member.added", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
20347
+ { type: "team.member.updated", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
20348
+ { type: "team.member.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
20349
+ { type: "team.resource.added", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
20350
+ { type: "team.resource.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
20351
+ { type: "work.created", domain: "works", aggregateType: "work", phase: "P1" },
20352
+ { type: "work.updated", domain: "works", aggregateType: "work", phase: "P1" },
20353
+ { type: "work.archived", domain: "works", aggregateType: "work", phase: "P1" },
20354
+ { type: "work.deleted", domain: "works", aggregateType: "work", phase: "P1" },
20355
+ { type: "work.member.added", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
20356
+ { type: "work.member.updated", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
20357
+ { type: "work.member.removed", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
20358
+ { type: "mission.created", domain: "missions", aggregateType: "mission", phase: "P2" },
20359
+ { type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
20360
+ { type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
20361
+ { type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
20362
+ { type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
20363
+ { type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
20364
+ { type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2" },
20365
+ { type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2" },
20366
+ { type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
20367
+ { type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
20368
+ { type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
20369
+ { type: "goal.archived", domain: "goals", aggregateType: "goal", phase: "P2" },
20370
+ { type: "goal.unarchived", domain: "goals", aggregateType: "goal", phase: "P2" },
20371
+ { type: "goal.deleted", domain: "goals", aggregateType: "goal", phase: "P2" },
20372
+ { type: "goal.progress_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
20373
+ { type: "idea.created", domain: "ideas", aggregateType: "idea", phase: "P2" },
20374
+ { type: "idea.updated", domain: "ideas", aggregateType: "idea", phase: "P2" },
20375
+ { type: "idea.status_changed", domain: "ideas", aggregateType: "idea", phase: "P2" },
20376
+ { type: "idea.deleted", domain: "ideas", aggregateType: "idea", phase: "P2" },
20377
+ { type: "task.created", domain: "tasks", aggregateType: "task", phase: "P1" },
20378
+ { type: "task.updated", domain: "tasks", aggregateType: "task", phase: "P1" },
20379
+ { type: "task.transitioned", domain: "tasks", aggregateType: "task", phase: "P1" },
20380
+ { type: "task.deleted", domain: "tasks", aggregateType: "task", phase: "P1" },
20381
+ { type: "task.assignee.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
20382
+ { type: "task.assignee.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
20383
+ { type: "task.watcher.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
20384
+ { type: "task.watcher.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
20385
+ { type: "task.block.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
20386
+ { type: "task.block.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
20387
+ { type: "task.relation.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
20388
+ { type: "task.relation.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
20389
+ { type: "task.comment.created", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
20390
+ { type: "task.comment.updated", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
20391
+ { type: "task.comment.deleted", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
20392
+ { type: "task.attachment.added", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
20393
+ { type: "task.attachment.removed", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
20394
+ { type: "task.runtime_updated", domain: "tasks", aggregateType: "task", phase: "P1", optIn: true },
20395
+ { type: "inbox.item.created", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
20396
+ { type: "inbox.item.answered", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
20397
+ { type: "inbox.item.state_changed", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
20398
+ { type: "inbox.item.deleted", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
20399
+ { type: "kb.document.created", domain: "kb", aggregateType: "kb_document", phase: "P3" },
20400
+ { type: "kb.document.updated", domain: "kb", aggregateType: "kb_document", phase: "P3" },
20401
+ { type: "kb.document.moved", domain: "kb", aggregateType: "kb_document", phase: "P3" },
20402
+ { type: "kb.document.archived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
20403
+ { type: "kb.document.unarchived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
20404
+ { type: "kb.document.deleted", domain: "kb", aggregateType: "kb_document", phase: "P3" },
20405
+ { type: "kb.document.review_changed", domain: "kb", aggregateType: "kb_document", phase: "P3" },
20406
+ { type: "kb.folder.created", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
20407
+ { type: "kb.folder.updated", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
20408
+ { type: "kb.folder.deleted", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
20409
+ { type: "email.message.received", domain: "email", aggregateType: "email_message", phase: "P4" },
20410
+ { type: "email.message.sent", domain: "email", aggregateType: "email_message", phase: "P4" },
20411
+ { type: "email.message.status_changed", domain: "email", aggregateType: "email_message", phase: "P4" },
20412
+ { type: "task.visibility_changed", domain: "visibility", aggregateType: "task", phase: "P1" },
20413
+ { type: "agent.visibility_changed", domain: "visibility", aggregateType: "agent", phase: "P1" },
20414
+ { type: "goal.visibility_changed", domain: "visibility", aggregateType: "goal", phase: "P2" },
20415
+ { type: "mission.visibility_changed", domain: "visibility", aggregateType: "mission", phase: "P2" },
20416
+ { type: "idea.visibility_changed", domain: "visibility", aggregateType: "idea", phase: "P2" }
20417
+ ];
20418
+ DOMAIN_EVENT_CATALOG3.map((entry) => entry.type);
20419
+ new Map(
20420
+ DOMAIN_EVENT_CATALOG3.map((entry) => [entry.type, entry])
20421
+ );
19905
20422
  var TerminalNotProvisionedError = class extends Error {
19906
20423
  static {
19907
20424
  __name(this, "TerminalNotProvisionedError");
@@ -22805,14 +23322,47 @@ async function runStart(deps, options) {
22805
23322
  });
22806
23323
  }
22807
23324
  await loop.start();
22808
- if (worker) {
22809
- await worker.start();
22810
- }
22811
- if (attendedWorker) {
22812
- await attendedWorker.start();
22813
- deps.out(
22814
- runtime.attended?.captureBackend ? "Live view enabled \u2014 the owner can watch this machine from the dashboard (screen and terminal)" : "Live view enabled \u2014 terminal only: no browser was found here to show a screen"
22815
- );
23325
+ let shuttingDown = false;
23326
+ const startLanes = /* @__PURE__ */ __name(async () => {
23327
+ if (worker) {
23328
+ await worker.start();
23329
+ }
23330
+ if (attendedWorker) {
23331
+ await attendedWorker.start();
23332
+ deps.out(
23333
+ runtime.attended?.captureBackend ? "Live view enabled \u2014 the owner can watch this machine from the dashboard (screen and terminal)" : "Live view enabled \u2014 terminal only: no browser was found here to show a screen"
23334
+ );
23335
+ }
23336
+ }, "startLanes");
23337
+ let lanesStarted = Promise.resolve();
23338
+ let laneStartFailed = false;
23339
+ let laneStartFailure = void 0;
23340
+ if (worker || attendedWorker) {
23341
+ if (loop.getState().state === "connected") {
23342
+ await startLanes();
23343
+ } else {
23344
+ deps.out(
23345
+ "Waiting for the platform to accept a heartbeat before leasing work (its answer carries this node\u2019s limits and version verdict)"
23346
+ );
23347
+ lanesStarted = startLanesAfterAcceptedHeartbeat(
23348
+ loop,
23349
+ async () => {
23350
+ deps.io.logger.info("Heartbeat accepted \u2014 starting the worker lanes");
23351
+ await startLanes();
23352
+ },
23353
+ () => !shuttingDown
23354
+ ).then((outcome) => {
23355
+ if (!outcome.failed) return;
23356
+ laneStartFailed = true;
23357
+ laneStartFailure = outcome.error;
23358
+ deps.io.logger.error(
23359
+ `Worker lanes failed to start after the first accepted heartbeat: ${describeError4(
23360
+ outcome.error
23361
+ )} \u2014 shutting down`
23362
+ );
23363
+ requestShutdown();
23364
+ });
23365
+ }
22816
23366
  }
22817
23367
  const reaper = worker && runtime.workspaceRoot ? startWorkspaceReaperTimer({
22818
23368
  rootPath: runtime.workspaceRoot,
@@ -22850,8 +23400,10 @@ async function runStart(deps, options) {
22850
23400
  }
22851
23401
  await (deps.waitForShutdown?.() ?? (deps.signals ? signalled : Promise.resolve()));
22852
23402
  reaper?.stop();
23403
+ shuttingDown = true;
22853
23404
  loop.stop();
22854
23405
  await loop.settled();
23406
+ await lanesStarted;
22855
23407
  if (attendedWorker) {
22856
23408
  for (const jobId of attendedWorker.getState().activeJobIds) {
22857
23409
  attendedWorker.cancelJob(jobId, "This machine is shutting down");
@@ -22864,8 +23416,29 @@ async function runStart(deps, options) {
22864
23416
  deps.out(`Worker host stopped (completed ${state.completed}, failed ${state.failed}).`);
22865
23417
  }
22866
23418
  deps.out("Stopped.");
23419
+ if (laneStartFailed) {
23420
+ throw new CliError(
23421
+ `Worker lanes failed to start after the first accepted heartbeat: ${describeError4(laneStartFailure)}`,
23422
+ EXIT_FAILURE
23423
+ );
23424
+ }
22867
23425
  }
22868
23426
  __name(runStart, "runStart");
23427
+ async function startLanesAfterAcceptedHeartbeat(loop, startLanes, shouldWait) {
23428
+ const accepted = await untilHeartbeatAccepted(loop, shouldWait);
23429
+ if (!accepted || !shouldWait()) return { started: false, failed: false };
23430
+ try {
23431
+ await startLanes();
23432
+ return { started: true, failed: false };
23433
+ } catch (error) {
23434
+ return { started: false, failed: true, error };
23435
+ }
23436
+ }
23437
+ __name(startLanesAfterAcceptedHeartbeat, "startLanesAfterAcceptedHeartbeat");
23438
+ function describeError4(error) {
23439
+ return error instanceof Error ? error.message : String(error);
23440
+ }
23441
+ __name(describeError4, "describeError");
22869
23442
  function parseConcurrency(raw) {
22870
23443
  if (raw === void 0) return void 0;
22871
23444
  const parsed = Number.parseInt(raw, 10);
@@ -23532,7 +24105,7 @@ function createLogger(options = {}) {
23532
24105
  __name(createLogger, "createLogger");
23533
24106
 
23534
24107
  // src/version.ts
23535
- var NODE_APP_VERSION = "0.2.2";
24108
+ var NODE_APP_VERSION = "0.2.4";
23536
24109
 
23537
24110
  // src/cli.ts
23538
24111
  function buildDeps() {