@inerrata-corporation/errata 2.0.6-dev.2008 → 2.0.6-dev.2028

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/errata.mjs +1796 -1758
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -77008,7 +77008,7 @@ function createWatchBreaker(deps) {
77008
77008
  }
77009
77009
 
77010
77010
  // src/engine.ts
77011
- var DAEMON_VERSION = true ? "2.0.6-dev.2008" : "2.0.0-alpha.0";
77011
+ var DAEMON_VERSION = true ? "2.0.6-dev.2028" : "2.0.0-alpha.0";
77012
77012
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
77013
77013
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
77014
77014
  var GIT_OP_MUTE_MS = 4e3;
@@ -79337,7 +79337,7 @@ init_identity2();
79337
79337
  // src/multi.ts
79338
79338
  init_dist();
79339
79339
  init_src5();
79340
- import { existsSync as existsSync30, readFileSync as readFileSync30, unlinkSync as unlinkSync3, writeFileSync as writeFileSync22 } from "node:fs";
79340
+ import { existsSync as existsSync31, readFileSync as readFileSync30, unlinkSync as unlinkSync3, writeFileSync as writeFileSync23 } from "node:fs";
79341
79341
 
79342
79342
  // src/principle-sync.ts
79343
79343
  init_src5();
@@ -82293,10 +82293,14 @@ function applyStopRequest(profile, now = () => /* @__PURE__ */ new Date()) {
82293
82293
  }
82294
82294
  };
82295
82295
  }
82296
- async function startHistoryStage(root, profile, client, walk10 = {}) {
82296
+ async function startHistoryStage(root, input, client, walk10 = {}) {
82297
+ let profile = input;
82297
82298
  if (!profile.projectId) return { started: false, reason: "no-project", profile };
82298
- const consents = historyConsents(profile);
82299
82299
  const response = await reportHistoryStage(client, profile);
82300
+ if (response?.consents && walk10.applyConsents) {
82301
+ profile = walk10.applyConsents(profile, response.consents);
82302
+ }
82303
+ const consents = historyConsents(profile);
82300
82304
  if (response?.stopRequested && profile.historyStage && profile.historyStage.state !== "refused") {
82301
82305
  const stopped = applyStopRequest(profile);
82302
82306
  await reportHistoryStage(client, stopped);
@@ -82307,1197 +82311,1610 @@ async function startHistoryStage(root, profile, client, walk10 = {}) {
82307
82311
  return { started: true, profile: outcome.profile };
82308
82312
  }
82309
82313
 
82310
- // src/multi.ts
82311
- init_registry();
82314
+ // src/link-flow.ts
82315
+ init_cloud_auth();
82316
+ import { createHash as createHash15 } from "node:crypto";
82317
+ import { execFileSync as execFileSync4 } from "node:child_process";
82318
+ import { existsSync as existsSync29, writeFileSync as writeFileSync22 } from "node:fs";
82319
+ import { join as join34 } from "node:path";
82312
82320
 
82313
- // src/adopt.ts
82314
- init_git_remote();
82315
- import { existsSync as existsSync29 } from "node:fs";
82316
- import { dirname as dirname12, join as join34 } from "node:path";
82317
- function findGitRoot(absPath) {
82318
- let dir = absPath;
82319
- for (let depth = 0; depth < 64; depth++) {
82320
- if (existsSync29(join34(dir, ".git"))) return dir;
82321
- const parent = dirname12(dir);
82322
- if (parent === dir) return null;
82323
- dir = parent;
82324
- }
82325
- return null;
82326
- }
82327
- var RETRY_COOLDOWN_MS = 15 * 6e4;
82328
- function createRootAdopter(deps) {
82329
- const attempted = /* @__PURE__ */ new Map();
82330
- let chain2 = Promise.resolve();
82331
- const tryAdopt = async (path2) => {
82332
- if (!deps.linkingAllowed()) return;
82333
- const root = findGitRoot(path2);
82334
- if (!root || deps.isKnownRoot(root)) return;
82335
- const last = attempted.get(root);
82336
- const now = Date.now();
82337
- if (last !== void 0 && now - last < RETRY_COOLDOWN_MS) return;
82338
- attempted.set(root, now);
82339
- if (!resolveGitDir(root)) return;
82340
- const locator = detectRepoLocator(root);
82341
- if (!locator) return;
82342
- const claimed = await deps.lookup(locator);
82343
- if (!claimed) return;
82344
- await deps.adopt(root);
82345
- deps.log(`[errata] adopted ${root} \u2014 ${locator} is claimed by your org (project ${claimed.id})`);
82346
- };
82321
+ // src/oauth-login.ts
82322
+ init_src7();
82323
+ init_cloud_auth();
82324
+ import { createServer } from "node:http";
82325
+ var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
82326
+ var strip = (u) => u.replace(/\/+$/, "");
82327
+ function deviceEndpoints(tokenEndpoint) {
82328
+ const token = new URL(tokenEndpoint);
82329
+ const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
82330
+ if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
82331
+ const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
82332
+ const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
82347
82333
  return {
82348
- noteMiss(path2) {
82349
- if (!path2) return;
82350
- chain2 = chain2.then(() => tryAdopt(path2)).catch(() => void 0);
82351
- },
82352
- idle: () => chain2
82334
+ code: at(`${prefix}/api/auth/device/code`),
82335
+ token: at(`${prefix}/api/auth/device/token`),
82336
+ resource: at(`${prefix}/api/gate/session-token`)
82353
82337
  };
82354
82338
  }
82355
-
82356
- // src/health-sentinel.ts
82357
- function numEnv4(key, fallback) {
82358
- const n = Number(process.env[key]);
82359
- return Number.isFinite(n) && n > 0 ? n : fallback;
82360
- }
82361
- var SENTINEL_TICK_MS = numEnv4("ERRATA_SENTINEL_TICK_MS", 3e4);
82362
- var SENTINEL_WINDOW_TICKS = numEnv4("ERRATA_SENTINEL_WINDOW_TICKS", 6);
82363
- var SENTINEL_BURN_CORES = numEnv4("ERRATA_SENTINEL_BURN_CORES", 1.2);
82364
- var SENTINEL_STORM_RATE = numEnv4("ERRATA_SENTINEL_STORM_RATE", 1e3);
82365
- var SENTINEL_STORM_TICKS = numEnv4("ERRATA_SENTINEL_STORM_TICKS", 4);
82366
- var SENTINEL_RENOTIFY_MS = numEnv4("ERRATA_SENTINEL_RENOTIFY_MS", 18e5);
82367
- var SENTINEL_CALM_TICKS = numEnv4("ERRATA_SENTINEL_CALM_TICKS", 2);
82368
- var SENTINEL_TOAST_COOLDOWN_MS = numEnv4("ERRATA_SENTINEL_TOAST_COOLDOWN_MS", 6e5);
82369
- function createHealthSentinel(deps = {}) {
82370
- const now = deps.now ?? Date.now;
82371
- const cpuUsage = deps.cpuUsage ?? (() => process.cpuUsage());
82372
- const readCensus = deps.readCensus ?? (() => {
82373
- const c = watchCensus();
82374
- return {
82375
- dispatched: c.dispatched,
82376
- logged: c.logged,
82377
- topOffender: c.topOffender ? { path: c.topOffender.path, count: c.topOffender.count } : null
82378
- };
82339
+ async function loginOAuthDevice(opts) {
82340
+ const fetchFn = opts.fetchFn ?? fetch;
82341
+ const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
82342
+ const device = deviceEndpoints(endpoints.tokenEndpoint);
82343
+ const clientId = oauthClientId();
82344
+ const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
82345
+ const codeResponse = await fetchFn(device.code, {
82346
+ method: "POST",
82347
+ headers: { "content-type": "application/json", accept: "application/json" },
82348
+ body: JSON.stringify({ client_id: clientId, scope })
82379
82349
  });
82380
- const activePasses = deps.activePasses ?? (() => passState().active.map((p) => p.name));
82381
- const rssMb = deps.rssMb ?? (() => Math.round(process.memoryUsage.rss() / 1048576));
82382
- const toast = deps.notify ?? ((body2, key) => notifyOpsEvent("daemon-distress", body2, { key }));
82383
- let prevCpu = cpuUsage();
82384
- let prevTs = now();
82385
- let prevDispatched = null;
82386
- let prevLogged = 0;
82387
- const coreSamples = [];
82388
- let stormTicks = 0;
82389
- let calmTicks = 0;
82390
- let distress = null;
82391
- let lastNotifiedAt = 0;
82392
- const lastToastByKind = /* @__PURE__ */ new Map();
82393
- const attribution = () => {
82394
- const parts2 = [];
82395
- const passes = activePasses();
82396
- if (passes.length > 0) parts2.push(`active pass: ${passes.join(", ")}`);
82397
- const c = readCensus();
82398
- if (c.topOffender) parts2.push(`hottest watch path: ${c.topOffender.path}`);
82399
- parts2.push(`rss ${rssMb()}MB`);
82400
- return parts2.join(" \xB7 ");
82401
- };
82402
- const maybeToast = (d, t) => {
82403
- if (t - (lastToastByKind.get(d.kind) ?? 0) < SENTINEL_TOAST_COOLDOWN_MS) return;
82404
- lastToastByKind.set(d.kind, t);
82405
- toast(d.headline, d.kind);
82406
- lastNotifiedAt = t;
82407
- };
82408
- const transition = (next) => {
82409
- const t = now();
82410
- if (next && !distress) {
82411
- distress = next;
82412
- deps.ledger?.("enter", next);
82413
- maybeToast(next, t);
82414
- deps.onChange?.(next);
82415
- } else if (next && distress) {
82416
- distress.body = next.body;
82417
- distress.headline = next.headline;
82418
- if (t - lastNotifiedAt >= SENTINEL_RENOTIFY_MS) maybeToast(distress, t);
82419
- } else if (!next && distress) {
82420
- deps.ledger?.("clear", distress);
82421
- distress = null;
82422
- deps.onChange?.(null);
82423
- }
82424
- };
82425
- const evaluate = () => {
82426
- const t = now();
82427
- const wallMs = Math.max(1, t - prevTs);
82428
- const cpu = cpuUsage();
82429
- const cores = (cpu.user + cpu.system - prevCpu.user - prevCpu.system) / 1e3 / wallMs;
82430
- prevCpu = cpu;
82431
- prevTs = t;
82432
- coreSamples.push(cores);
82433
- if (coreSamples.length > SENTINEL_WINDOW_TICKS) coreSamples.shift();
82434
- const c = readCensus();
82435
- const dispatchRate = prevDispatched === null ? 0 : (c.dispatched - prevDispatched) / wallMs * 1e3;
82436
- const survived = c.logged - prevLogged;
82437
- prevDispatched = c.dispatched;
82438
- prevLogged = c.logged;
82439
- stormTicks = dispatchRate > SENTINEL_STORM_RATE && survived === 0 ? stormTicks + 1 : 0;
82440
- let wanted = null;
82441
- if (stormTicks >= SENTINEL_STORM_TICKS) {
82442
- wanted = {
82443
- kind: "watch-storm",
82444
- headline: `errata's file watcher is stuck busy (~${Math.round(dispatchRate)} events/sec with nothing changing) \u2014 run \`errata status\` for detail`,
82445
- body: `filesystem watcher is processing ~${Math.round(dispatchRate)} events/sec with NOTHING surviving \u2014 a wedged or ignored-tree watch is burning CPU for no signal \xB7 ${attribution()}`,
82446
- sinceTs: distress?.kind === "watch-storm" ? distress.sinceTs : t
82447
- };
82448
- } else {
82449
- const windowFull = coreSamples.length >= SENTINEL_WINDOW_TICKS;
82450
- const avgCores = coreSamples.reduce((a, b) => a + b, 0) / Math.max(1, coreSamples.length);
82451
- if (windowFull && avgCores >= SENTINEL_BURN_CORES && cores >= SENTINEL_BURN_CORES && activePasses().length === 0) {
82452
- wanted = {
82453
- kind: "cpu-burn",
82454
- headline: `errata is using more CPU than expected (${avgCores.toFixed(1)} cores with no work running) \u2014 run \`errata status\` for detail`,
82455
- body: `sustained CPU with no pass running \u2014 avg ${avgCores.toFixed(1)} cores \xB7 ${attribution()}`,
82456
- sinceTs: distress?.kind === "cpu-burn" ? distress.sinceTs : t
82457
- };
82350
+ if (!codeResponse.ok) {
82351
+ throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
82352
+ }
82353
+ const code = await codeResponse.json();
82354
+ if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
82355
+ throw new Error("device authorization returned an incomplete response");
82356
+ }
82357
+ opts.onVerification?.({
82358
+ url: code.verification_uri_complete ?? code.verification_uri,
82359
+ userCode: code.user_code,
82360
+ expiresInSec: code.expires_in
82361
+ });
82362
+ const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
82363
+ let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
82364
+ let deviceSession = null;
82365
+ let approvedInstallation = null;
82366
+ while (Date.now() < deadline) {
82367
+ await (opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(intervalMs);
82368
+ const poll = await fetchFn(device.token, {
82369
+ method: "POST",
82370
+ headers: { "content-type": "application/json", accept: "application/json" },
82371
+ body: JSON.stringify({
82372
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
82373
+ device_code: code.device_code,
82374
+ client_id: clientId
82375
+ })
82376
+ });
82377
+ const payload = await poll.json().catch(() => null);
82378
+ if (!poll.ok) {
82379
+ const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
82380
+ if (error48 === "authorization_pending") continue;
82381
+ if (error48 === "slow_down") {
82382
+ intervalMs += 5e3;
82383
+ continue;
82458
82384
  }
82385
+ throw new Error(`device authorization failed: ${error48}`);
82459
82386
  }
82460
- if (wanted) {
82461
- calmTicks = 0;
82462
- transition(wanted);
82463
- } else if (distress) {
82464
- calmTicks++;
82465
- if (calmTicks >= SENTINEL_CALM_TICKS) {
82466
- calmTicks = 0;
82467
- transition(null);
82468
- }
82469
- }
82470
- };
82471
- const interval = deps.scheduleTicks === false ? null : setInterval(() => {
82472
- try {
82473
- evaluate();
82474
- } catch {
82475
- }
82476
- }, SENTINEL_TICK_MS);
82477
- interval?.unref?.();
82478
- return {
82479
- current: () => distress ? { ...distress } : null,
82480
- tick: evaluate,
82481
- stop: () => {
82482
- if (interval) clearInterval(interval);
82387
+ deviceSession = typeof payload?.["access_token"] === "string" ? payload["access_token"] : null;
82388
+ approvedInstallation = typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null;
82389
+ break;
82390
+ }
82391
+ if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
82392
+ if (approvedInstallation !== opts.installationId) {
82393
+ throw new Error("approved installation does not match the terminal request");
82394
+ }
82395
+ const resourceResponse = await fetchFn(device.resource, {
82396
+ method: "POST",
82397
+ headers: {
82398
+ authorization: `Bearer ${deviceSession}`,
82399
+ "content-type": "application/json",
82400
+ accept: "application/json"
82401
+ },
82402
+ body: JSON.stringify({
82403
+ installationId: approvedInstallation,
82404
+ clientId,
82405
+ scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
82406
+ })
82407
+ });
82408
+ const resource = await resourceResponse.json().catch(() => null);
82409
+ if (!resourceResponse.ok || typeof resource?.["access_token"] !== "string") {
82410
+ throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
82411
+ }
82412
+ return {
82413
+ installationId: approvedInstallation,
82414
+ clientId,
82415
+ tokenEndpoint: device.resource,
82416
+ tokens: {
82417
+ accessToken: resource["access_token"],
82418
+ refreshToken: deviceSession,
82419
+ ...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
82483
82420
  }
82484
82421
  };
82485
82422
  }
82486
-
82487
- // src/codex-wake.ts
82488
- var CODEX_WAKE_LOOKBACK_MS = 10 * 6e4;
82489
- var CODEX_WAKE_BOOT_LOOKBACK_MS = 24 * 60 * 6e4;
82490
- function codexWakeCandidates(input) {
82491
- if (input.disabled) return [];
82492
- const find = input.find ?? ((cwd, since) => codexRolloutsForCwd(cwd, { sinceMs: since, limit: 1 }).length + geminiTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length + opencodeTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length);
82493
- const out2 = [];
82494
- for (const entry of input.entries) {
82495
- if (input.liveRoots.has(input.normPath(entry.path))) continue;
82496
- let n = 0;
82497
- try {
82498
- n = find(entry.path, input.sinceMs);
82499
- } catch {
82500
- continue;
82423
+ async function discoverEndpoints(cloudUrl, fetchFn = fetch) {
82424
+ let consoleUrl = cloudUrl;
82425
+ try {
82426
+ const prm = await fetchFn(`${strip(cloudUrl)}/.well-known/oauth-protected-resource`);
82427
+ if (prm.ok) {
82428
+ const j = await prm.json();
82429
+ if (j.authorization_servers?.[0]) consoleUrl = j.authorization_servers[0];
82501
82430
  }
82502
- if (n > 0) out2.push({ name: entry.name, path: entry.path });
82431
+ } catch {
82503
82432
  }
82504
- return out2;
82505
- }
82506
-
82507
- // src/multi.ts
82508
- var projectSymbolSalts = /* @__PURE__ */ new Map();
82509
- async function resolveProjectSymbolSalt(client, projectId) {
82510
- const cached2 = projectSymbolSalts.get(projectId);
82511
- if (cached2) return { projectId, salt: cached2 };
82512
- const { salt } = await client.projectSymbolSalt(projectId);
82513
- projectSymbolSalts.set(projectId, salt);
82514
- return { projectId, salt };
82515
- }
82516
- function normPath(p) {
82517
- return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
82518
- }
82519
- function contextKey(p) {
82520
- return `${[...p.stack].sort().join(",")}|${p.domains[0] ?? p.languages[0] ?? "?"}`;
82521
- }
82522
- function pickOwnerRoot(roots, absPath) {
82523
- if (!absPath) return null;
82524
- const n = normPath(absPath);
82525
- let best = null;
82526
- let bestLen = -1;
82527
- for (const root of roots) {
82528
- const r = normPath(root);
82529
- if ((n === r || n.startsWith(r + "/")) && r.length > bestLen) {
82530
- best = root;
82531
- bestLen = r.length;
82433
+ try {
82434
+ const asm = await fetchFn(`${strip(consoleUrl)}/.well-known/oauth-authorization-server`);
82435
+ if (asm.ok) {
82436
+ const j = await asm.json();
82437
+ if (j.authorization_endpoint && j.token_endpoint) {
82438
+ return {
82439
+ authorizationEndpoint: j.authorization_endpoint,
82440
+ tokenEndpoint: j.token_endpoint,
82441
+ ...j.registration_endpoint ? { registrationEndpoint: j.registration_endpoint } : {}
82442
+ };
82443
+ }
82532
82444
  }
82445
+ } catch {
82533
82446
  }
82534
- return best;
82447
+ const base = strip(consoleUrl);
82448
+ return {
82449
+ authorizationEndpoint: `${base}/api/auth/oauth2/authorize`,
82450
+ tokenEndpoint: `${base}/api/auth/oauth2/token`
82451
+ };
82535
82452
  }
82536
- function ownerOf(records, absPath) {
82537
- const root = pickOwnerRoot(records.map((r) => r.root), absPath);
82538
- return root == null ? null : records.find((r) => r.root === root) ?? null;
82453
+ function buildAuthorizeUrl(endpoint, p) {
82454
+ const u = new URL(endpoint);
82455
+ u.searchParams.set("response_type", "code");
82456
+ u.searchParams.set("client_id", p.clientId);
82457
+ u.searchParams.set("redirect_uri", p.redirectUri);
82458
+ u.searchParams.set("scope", p.scope);
82459
+ u.searchParams.set("state", p.state);
82460
+ u.searchParams.set("code_challenge", p.challenge);
82461
+ u.searchParams.set("code_challenge_method", p.method);
82462
+ return u.toString();
82539
82463
  }
82540
- async function startMultiDaemon(opts = {}) {
82541
- const lockPath = globalDaemonLock();
82542
- if (opts.enforceSingleton !== false && isDaemonAlive(lockPath)) {
82543
- throw new Error(
82544
- `an errata daemon is already running (lock: ${lockPath}). Run \`errata stop\` first, or attach to the running one.`
82545
- );
82546
- }
82547
- const cfg = loadConfig();
82548
- const CLOUD_TIMEOUT_MS = 12e4;
82549
- const initialCloudUrl = normalizeCloudUrl(opts.cloudUrl ?? cfg.cloudUrl);
82550
- const initialCfg = { ...cfg, cloudUrl: initialCloudUrl };
82551
- if (opts.apiKey) {
82552
- initialCfg.apiKey = opts.apiKey;
82553
- initialCfg.accessToken = null;
82554
- initialCfg.refreshToken = null;
82555
- initialCfg.tokenEndpoint = null;
82556
- initialCfg.oauthClientId = null;
82557
- }
82558
- const cloud = authedCloudClient(initialCfg, { timeoutMs: CLOUD_TIMEOUT_MS, daemonVersion: DAEMON_VERSION });
82559
- const cloudNow = () => {
82560
- const c = loadConfig();
82561
- const baseUrl2 = normalizeCloudUrl(opts.cloudUrl ?? c.cloudUrl);
82562
- const current = { ...c, cloudUrl: baseUrl2 };
82563
- if (opts.apiKey) {
82564
- current.apiKey = opts.apiKey;
82565
- current.accessToken = null;
82566
- current.refreshToken = null;
82567
- current.tokenEndpoint = null;
82568
- current.oauthClientId = null;
82569
- }
82570
- return authedCloudClient(current, { timeoutMs: CLOUD_TIMEOUT_MS, daemonVersion: DAEMON_VERSION });
82571
- };
82572
- const identityFor = (entry) => {
82573
- if (opts.apiKey) {
82574
- return {
82575
- profileName: null,
82576
- profile: null,
82577
- rule: "env-config-path",
82578
- invalid: false,
82579
- reason: "daemon started with an explicit --api-key (process-pinned)"
82580
- };
82581
- }
82582
- return resolveWorkspaceIdentity(loadConfig(), entry);
82583
- };
82584
- const identityBlocks = /* @__PURE__ */ new Map();
82585
- const noteIdentityBlock = (wsId, wsName, identity) => {
82586
- if (!identityBlocks.has(wsId)) {
82587
- identityBlocks.set(wsId, {
82588
- workspace: wsName,
82589
- profileName: identity.profileName,
82590
- reason: identity.reason,
82591
- since: Date.now()
82464
+ async function loginOAuthLoopback(opts) {
82465
+ const fetchFn = opts.fetchFn ?? fetch;
82466
+ const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
82467
+ const pkce = generatePkce();
82468
+ const state = randomState();
82469
+ const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
82470
+ const { code, redirectUri, clientId } = await new Promise(
82471
+ (resolve7, reject) => {
82472
+ const server = createServer((req, res) => {
82473
+ const u = new URL(req.url ?? "/", "http://127.0.0.1");
82474
+ if (u.pathname !== "/callback") {
82475
+ res.writeHead(404);
82476
+ res.end();
82477
+ return;
82478
+ }
82479
+ const addr2 = server.address();
82480
+ const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
82481
+ const err2 = u.searchParams.get("error");
82482
+ const code2 = u.searchParams.get("code");
82483
+ const returnedState = u.searchParams.get("state");
82484
+ if (err2 || !code2 || returnedState !== state) {
82485
+ res.writeHead(400, { "content-type": "text/plain" });
82486
+ res.end(`login failed: ${err2 ?? (returnedState !== state ? "state_mismatch" : "no_code")}`);
82487
+ server.close();
82488
+ reject(new Error(err2 ?? "invalid_callback"));
82489
+ return;
82490
+ }
82491
+ res.writeHead(200, { "content-type": "text/html" });
82492
+ res.end("<html><body>inErrata: you're logged in. You can close this tab.</body></html>");
82493
+ server.close();
82494
+ resolve7({ code: code2, redirectUri: `http://127.0.0.1:${port}/callback`, clientId: activeClientId });
82592
82495
  });
82593
- console.error(`[identity] HOLDING cloud traffic for workspace "${wsName}": ${identity.reason}`);
82594
- }
82595
- };
82596
- const clearIdentityBlock = (wsId) => {
82597
- if (identityBlocks.delete(wsId)) console.log(`[identity] workspace unblocked \u2014 spooled capture drains on this pass`);
82598
- };
82599
- const projectHolds = /* @__PURE__ */ new Map();
82600
- const noteProjectHold = (wsId, wsName, reason) => {
82601
- if (!projectHolds.has(wsId)) {
82602
- projectHolds.set(wsId, { workspace: wsName, reason, since: Date.now() });
82603
- console.error(
82604
- `[project] HOLDING cloud push for workspace "${wsName}": cannot prove its project \u2014 ${reason}. Shipping without the assertion would publish project-scoped knowledge to the whole org; the batch stays pending instead.`
82605
- );
82606
- }
82607
- };
82608
- const clearProjectHold = (wsId) => {
82609
- if (projectHolds.delete(wsId)) console.log(`[project] project assertion restored \u2014 held contributions drain on this pass`);
82610
- };
82611
- const cloudForIdentity = (identity) => {
82612
- if (identity.invalid) return blockedCloudClient(identity.reason);
82613
- if (identity.rule === "workspace-binding" && identity.profileName) {
82614
- return authedCloudClientForProfile(loadConfig(), identity.profileName, {
82615
- timeoutMs: CLOUD_TIMEOUT_MS,
82616
- daemonVersion: DAEMON_VERSION
82496
+ let activeClientId = oauthClientId();
82497
+ server.on("error", reject);
82498
+ server.listen(0, "127.0.0.1", () => {
82499
+ void (async () => {
82500
+ try {
82501
+ const addr2 = server.address();
82502
+ const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
82503
+ const redirectUri2 = `http://127.0.0.1:${port}/callback`;
82504
+ if (endpoints.registrationEndpoint) {
82505
+ const registered = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
82506
+ redirectUri: redirectUri2,
82507
+ scope,
82508
+ clientName: "errata daemon"
82509
+ });
82510
+ activeClientId = registered.clientId;
82511
+ }
82512
+ const url2 = buildAuthorizeUrl(endpoints.authorizationEndpoint, {
82513
+ clientId: activeClientId,
82514
+ redirectUri: redirectUri2,
82515
+ scope,
82516
+ state,
82517
+ challenge: pkce.challenge,
82518
+ method: pkce.method
82519
+ });
82520
+ if (opts.open) opts.open(url2);
82521
+ else console.log(`approve this login in your browser:
82522
+ ${url2}`);
82523
+ } catch (err2) {
82524
+ server.close();
82525
+ reject(err2);
82526
+ }
82527
+ })();
82617
82528
  });
82618
- }
82619
- return cloudNow();
82620
- };
82621
- const sharedDbPath = sharedStorePath();
82622
- ensureParent(sharedDbPath);
82623
- const sharedStore = openGraphStore({ path: sharedDbPath });
82624
- const bundledRun = import.meta.url.endsWith(".mjs");
82625
- const consolidateWorker = bundledRun && sharedDbPath !== ":memory:" ? new ConsolidateWorker() : null;
82626
- startLoopLagMonitor();
82627
- let baseUrl = "";
82628
- const records = [];
82629
- const mountedIds = /* @__PURE__ */ new Set();
82630
- const machineDominantLanguage = () => {
82631
- const langCounts = /* @__PURE__ */ new Map();
82632
- for (const r of records) {
82633
- for (const l of r.engine.profile.languages ?? []) {
82634
- const k = String(l).trim().toLowerCase();
82635
- if (k) langCounts.set(k, (langCounts.get(k) ?? 0) + 1);
82636
- }
82637
- }
82638
- return [...langCounts.entries()].sort(
82639
- (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)
82640
- )[0]?.[0];
82641
- };
82642
- const updatePoller = opts.updateCheck ? startUpdatePoller({
82643
- channel: loadConfig().updateChannel,
82644
- onChange: (pending) => {
82645
- if (pending) {
82646
- console.log(`\u26A1 errata update available: ${pending.current} \u2192 ${pending.latest} \u2014 run: errata update`);
82647
- }
82648
- for (const r of records) r.engine.markContextDirty();
82649
- }
82650
- }) : null;
82651
- const healthSentinel = createHealthSentinel({
82652
- onChange: () => {
82653
- for (const r of records) r.engine.markContextDirty();
82654
- },
82655
- ledger: (event, d) => {
82656
- const dir = records[0]?.engine.paths.configDir;
82657
- if (dir) {
82658
- appendPassLedger(dir, "health-sentinel", 0, {
82659
- [`distress:${event}:${d.kind}`]: 1,
82660
- distressSinceTs: d.sinceTs
82661
- });
82529
+ if (opts.timeoutMs) {
82530
+ setTimeout(() => {
82531
+ server.close();
82532
+ reject(new Error("login timed out \u2014 run `errata login` again"));
82533
+ }, opts.timeoutMs).unref?.();
82662
82534
  }
82663
82535
  }
82536
+ );
82537
+ const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
82538
+ code,
82539
+ codeVerifier: pkce.verifier,
82540
+ redirectUri,
82541
+ clientId
82664
82542
  });
82665
- let serverPending = null;
82666
- const boundaryListeners = [];
82667
- let boundaryFlushing = false;
82668
- let settleRunning = false;
82669
- let boundaryFlushStartedAt = 0;
82670
- let boundaryFlushStage = "";
82671
- let boundaryFlushUnits = 0;
82672
- let boundaryFlushLastAdvanceAt = 0;
82673
- let boundaryFlushBatches = 0;
82674
- const beginFlush = (stage2) => {
82675
- boundaryFlushing = true;
82676
- boundaryFlushStartedAt = Date.now();
82677
- boundaryFlushLastAdvanceAt = Date.now();
82678
- boundaryFlushUnits = 0;
82679
- boundaryFlushBatches = 0;
82680
- boundaryFlushItems = 0;
82681
- boundaryFlushDetail = "";
82682
- boundaryFlushStage = stage2;
82683
- };
82684
- const flushStage = (stage2) => {
82685
- boundaryFlushStage = stage2;
82686
- };
82687
- let boundaryFlushItems = 0;
82688
- let boundaryFlushDetail = "";
82689
- const noteFlushProgress = (units, items = 0, detail = "") => {
82690
- boundaryFlushBatches++;
82691
- boundaryFlushUnits += Math.max(0, units);
82692
- boundaryFlushItems += Math.max(0, items);
82693
- if (detail) boundaryFlushDetail = detail;
82694
- if (units > 0 || items > 0) boundaryFlushLastAdvanceAt = Date.now();
82543
+ return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
82544
+ }
82545
+
82546
+ // src/link-flow.ts
82547
+ function remoteFingerprint(locator) {
82548
+ return createHash15("sha256").update(locator).digest("hex");
82549
+ }
82550
+ function isDecision(value) {
82551
+ if (!value || typeof value !== "object") return false;
82552
+ const v = value;
82553
+ return typeof v["projectId"] === "string" && typeof v["locator"] === "string" && (v["visibility"] === "public" || v["visibility"] === "private") && typeof v["consents"] === "object" && v["consents"] !== null;
82554
+ }
82555
+ async function linkThroughConsole(opts) {
82556
+ const fetchFn = opts.fetchFn ?? fetch;
82557
+ const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
82558
+ const device = deviceEndpoints(endpoints.tokenEndpoint);
82559
+ const clientId = oauthClientId();
82560
+ const link = {
82561
+ locator: opts.facts.locator,
82562
+ remoteFingerprint: remoteFingerprint(opts.facts.locator),
82563
+ ...opts.facts.name ? { name: opts.facts.name } : {},
82564
+ ...opts.facts.rootCommitSha ? { rootCommitSha: opts.facts.rootCommitSha } : {},
82565
+ ...opts.facts.installationId ? { installationId: opts.facts.installationId } : {},
82566
+ ...opts.facts.agents && opts.facts.agents.length > 0 ? { agents: opts.facts.agents.slice(0, 12) } : {},
82567
+ ...typeof opts.facts.commitCount === "number" ? { commitCount: opts.facts.commitCount } : {}
82695
82568
  };
82696
- const flushSnapshot = () => ({
82697
- running: boundaryFlushing,
82698
- stage: boundaryFlushStage,
82699
- detail: boundaryFlushDetail,
82700
- ageMs: boundaryFlushStartedAt ? Date.now() - boundaryFlushStartedAt : 0,
82701
- units: boundaryFlushUnits,
82702
- items: boundaryFlushItems,
82703
- chunks: boundaryFlushBatches,
82704
- stalledMs: boundaryFlushLastAdvanceAt ? Date.now() - boundaryFlushLastAdvanceAt : 0,
82705
- // Held workspaces, so a hold is REPORTABLE and not merely logged once at the
82706
- // transition. `errata status` reads this rather than the on-disk profile:
82707
- // the profile records what the workspace INTENDS to assert, which is why
82708
- // status could print "→ project 019f462d" for hours while every batch was
82709
- // being held or shipped unlinked. A hold is the daemon's actual behavior.
82710
- holds: [
82711
- ...[...projectHolds.values()].map((h) => ({
82712
- workspace: h.workspace,
82713
- kind: "project",
82714
- reason: h.reason,
82715
- sinceMs: Date.now() - h.since
82716
- })),
82717
- ...[...identityBlocks.values()].map((h) => ({
82718
- workspace: h.workspace,
82719
- kind: "identity",
82720
- reason: h.reason,
82721
- sinceMs: Date.now() - h.since
82722
- }))
82723
- ]
82569
+ const codeResponse = await fetchFn(device.code, {
82570
+ method: "POST",
82571
+ headers: { "content-type": "application/json", accept: "application/json" },
82572
+ body: JSON.stringify({ client_id: clientId, scope: "project:link", link })
82724
82573
  });
82725
- const endFlush = () => {
82726
- boundaryFlushing = false;
82727
- boundaryFlushStage = "";
82728
- };
82729
- const fireBoundary = () => {
82730
- for (const l of boundaryListeners) {
82731
- try {
82732
- l();
82733
- } catch {
82734
- }
82735
- }
82736
- };
82737
- const buildRecord = (entry) => {
82738
- const id = entry.id;
82739
- const wsIdentity = identityFor(entry);
82740
- let wsCloud = cloud;
82741
- if (wsIdentity.rule === "workspace-binding") {
82742
- if (wsIdentity.invalid) {
82743
- noteIdentityBlock(entry.id, entry.name, wsIdentity);
82744
- wsCloud = blockedCloudClient(wsIdentity.reason);
82745
- } else {
82746
- wsCloud = authedCloudClientForProfile(loadConfig(), wsIdentity.profileName, {
82747
- timeoutMs: CLOUD_TIMEOUT_MS,
82748
- daemonVersion: DAEMON_VERSION
82749
- });
82750
- console.log(`[identity] workspace "${entry.name}" bound \u2192 profile "${wsIdentity.profileName}"`);
82751
- }
82574
+ if (!codeResponse.ok) {
82575
+ const text = (await codeResponse.text()).slice(0, 200);
82576
+ if (codeResponse.status === 404) {
82577
+ throw new Error("this console does not offer the link flow yet (device authorization is off)");
82752
82578
  }
82753
- const engine = createWorkspaceEngine({
82754
- workspaceRoot: entry.path,
82755
- cloud: wsCloud,
82756
- apiKey: opts.apiKey,
82757
- reviewUrl: () => `${baseUrl}/ws/${id}/review`,
82758
- sharedStore,
82759
- pendingUpdate: () => serverPending ?? updatePoller?.current() ?? null,
82760
- // Ops-gated (default OFF): the 🚨 notice reads as "errata broke my
82761
- // machine" to a non-operator. Detection + ledger run regardless — only
82762
- // this push surface (and the toast, gated in notifyOpsEvent) is scoped.
82763
- daemonDistress: () => loadConfig().opsNotices ? healthSentinel.current() : null,
82764
- ...opts.skipWatchers ? { skipWatchers: true } : {}
82765
- });
82766
- const webApp = buildWebUi({
82767
- paths: engine.paths,
82768
- store: engine.store,
82769
- log: engine.log,
82770
- profile: engine.profile,
82771
- daemonVersion: DAEMON_VERSION,
82772
- basePath: `/ws/${id}`,
82773
- onWorkingFileChanged: engine.webHooks.onWorkingFileChanged,
82774
- onHook: engine.webHooks.onHook,
82775
- onHookEvent: engine.webHooks.onHookEvent,
82776
- onShellEvent: engine.webHooks.onShellEvent,
82777
- // The tool-run RECORD seam (SP-tool-ground). The single-engine path wired
82778
- // this in #1172; this sibling builder never did, so on every multi
82779
- // deployment (i.e. every real one) the webui logged "tool-run recorded"
82780
- // and dropped the record on the floor — toolSessionsTracked read 0 across
82781
- // 936 turns while the log showed records landing (VS-corroboration-rate).
82782
- onToolsObserved: engine.webHooks.onToolsObserved,
82783
- onToolError: engine.webHooks.onToolError,
82784
- onDiagnostics: engine.webHooks.onDiagnostics,
82785
- onTurn: engine.webHooks.onTurn,
82786
- onMessage: engine.webHooks.onMessage,
82787
- onSessionEnd: (e) => {
82788
- engine.webHooks.onSessionEnd(e);
82789
- fireBoundary();
82790
- }
82791
- });
82792
- return { entry, engine, webApp, root: entry.path, id, lastHookAt: Date.now() };
82793
- };
82794
- const pruned = pruneMissingWorkspaces();
82795
- if (pruned.length > 0) {
82796
- console.log(`[errata] pruned ${pruned.length} registered workspace(s) whose path no longer exists`);
82579
+ throw new Error(`link request failed: HTTP ${codeResponse.status} ${text}`);
82797
82580
  }
82798
- if (registryCorrupt()) {
82799
- console.error("[identity] workspaces.json is UNREADABLE \u2014 all per-workspace cloud push will hold until it parses (fail-closed)");
82800
- } else {
82801
- for (const b of boundWorkspaceEntries()) {
82802
- console.log(`[identity] bound workspace: ${b.name} \u2192 profile "${b.installationProfile}"`);
82803
- }
82581
+ const code = await codeResponse.json();
82582
+ if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
82583
+ throw new Error("link request returned an incomplete response");
82804
82584
  }
82805
- const entries = listWorkspaceEntries();
82806
- if (opts.eagerWorkspaces ?? EAGER_WORKSPACES_AT_BOOT) {
82807
- for (const entry of entries) {
82808
- try {
82809
- records.push(buildRecord(entry));
82810
- } catch (err2) {
82811
- console.warn(`[errata] skipping ${entry.name} (${entry.path}): ${err2 instanceof Error ? err2.message : err2}`);
82585
+ opts.onVerification?.({
82586
+ url: code.verification_uri_complete ?? code.verification_uri,
82587
+ userCode: code.user_code,
82588
+ expiresInSec: code.expires_in
82589
+ });
82590
+ const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
82591
+ let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
82592
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
82593
+ while (Date.now() < deadline) {
82594
+ await sleep(intervalMs);
82595
+ const poll = await fetchFn(device.token, {
82596
+ method: "POST",
82597
+ headers: { "content-type": "application/json", accept: "application/json" },
82598
+ body: JSON.stringify({
82599
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
82600
+ device_code: code.device_code,
82601
+ client_id: clientId
82602
+ })
82603
+ });
82604
+ const payload = await poll.json().catch(() => null);
82605
+ if (!poll.ok) {
82606
+ const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
82607
+ if (error48 === "authorization_pending") continue;
82608
+ if (error48 === "slow_down") {
82609
+ intervalMs += 5e3;
82610
+ continue;
82812
82611
  }
82612
+ if (error48 === "access_denied") return { status: "denied" };
82613
+ if (error48 === "expired_token") return { status: "expired" };
82614
+ throw new Error(`link approval failed: ${error48}`);
82813
82615
  }
82814
- if (entries.length > 0 && records.length < entries.length) {
82815
- const failed = entries.length - records.length;
82816
- console.warn(
82817
- `[errata] \u26A0 ${failed} of ${entries.length} project(s) failed to start \u2014 likely a stale daemon holding DB locks. Run \`errata stop\` (or kill the old errata process) and retry.`
82818
- );
82616
+ const decision = payload?.["link"];
82617
+ if (!isDecision(decision)) {
82618
+ throw new Error("the console approved the device but returned no link decision");
82819
82619
  }
82820
- } else if (entries.length > 0) {
82821
- console.log(
82822
- `[errata] ${entries.length} workspace(s) registered \xB7 cold \u2014 an engine spawns on the first hook from each`
82823
- );
82620
+ return {
82621
+ status: "approved",
82622
+ decision,
82623
+ installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
82624
+ };
82824
82625
  }
82825
- const app = new Hono2();
82826
- const attachWorkspace = (root) => {
82827
- const norm = normPath(root);
82828
- if (records.some((r) => normPath(r.root) === norm)) return { attached: false, id: null, reason: "already-watched" };
82829
- const entry = listWorkspaceEntries().find((e) => normPath(e.path) === norm);
82830
- if (!entry) return { attached: false, id: null, reason: "not-registered" };
82831
- let rec;
82832
- try {
82833
- rec = buildRecord(entry);
82834
- } catch (err2) {
82835
- return { attached: false, id: null, reason: err2 instanceof Error ? err2.message : "build-failed" };
82836
- }
82837
- records.push(rec);
82838
- void ambientLinkAll();
82626
+ return { status: "expired" };
82627
+ }
82628
+ function collectRepoFacts(root) {
82629
+ const git3 = (args2) => {
82839
82630
  try {
82840
- writeFileSync22(
82841
- rec.engine.paths.daemonLock,
82842
- JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${rec.id}`, startedAt: Date.now() }),
82843
- "utf8"
82844
- );
82845
- registerWorkspace(rec.engine.profile, rec.root);
82631
+ return execFileSync4("git", args2, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
82846
82632
  } catch {
82633
+ return null;
82847
82634
  }
82848
- void rec.engine.reindex({ skipEmbed: true, quiet: rec.engine.store.nodeCount() > 0 }).catch(() => {
82849
- });
82850
- console.log(`[errata] hot-registered ${rec.entry.name} (${rec.id}) \u2014 now watching live`);
82851
- return { attached: true, id: rec.id };
82852
82635
  };
82853
- app.post("/api/attach", async (c) => {
82854
- let body2 = {};
82855
- try {
82856
- body2 = await c.req.json();
82857
- } catch {
82858
- return c.json({ error: "bad-json" }, 400);
82859
- }
82860
- const root = typeof body2["path"] === "string" ? body2["path"] : null;
82861
- if (!root) return c.json({ error: "path-required" }, 400);
82862
- return c.json(attachWorkspace(root));
82863
- });
82864
- app.post("/api/notify-test", (c) => {
82865
- notify("Errata \xB7 test", "Fired from the daemon \u2014 if you see this, daemon toasts render.");
82866
- return c.json({ ok: true });
82867
- });
82868
- app.get(
82869
- "/",
82870
- (c) => c.json({
82871
- daemonVersion: DAEMON_VERSION,
82872
- pendingUpdate: updatePoller?.current() ?? null,
82873
- projects: records.map((r) => ({
82874
- id: r.id,
82875
- name: r.entry.name,
82876
- path: r.root,
82877
- stack: r.entry.stack,
82878
- nodes: r.engine.store.nodeCount(),
82879
- // COUNT(*) — the materializing findNodesByLabel(...).length form held
82880
- // the event loop for seconds per poll once stores grew (HZ-index-hydrate).
82881
- problems: r.engine.store.countNodesByLabel("Problem"),
82882
- solutions: r.engine.store.countNodesByLabel("Solution"),
82883
- endpoints: `/ws/${r.id}/`
82884
- })),
82885
- humanView: "run `errata report` \u2014 the dashboard was retired (GRAFT 4e)"
82886
- })
82887
- );
82888
- const STRANDED_TTL_MS = 6e4;
82889
- const strandedCache = /* @__PURE__ */ new Map();
82890
- const strandedRefreshing = /* @__PURE__ */ new Set();
82891
- const strandedCount = (r) => {
82892
- const hit = strandedCache.get(r.id);
82893
- if (hit && Date.now() - hit.at < STRANDED_TTL_MS) return hit.count;
82894
- if (!strandedRefreshing.has(r.id)) {
82895
- strandedRefreshing.add(r.id);
82896
- const finish = (count) => {
82897
- strandedCache.set(r.id, { at: Date.now(), count });
82898
- strandedRefreshing.delete(r.id);
82899
- };
82900
- if (consolidateWorker && r.engine.paths.castalia !== ":memory:") {
82901
- consolidateWorker.run({ kind: "strandedCount", sharedDbPath, castaliaPath: r.engine.paths.castalia }).then((n) => finish(Number(n))).catch(() => {
82902
- setImmediate(() => {
82903
- const done = markPass(`stranded-scan:${r.entry.name}`);
82904
- try {
82905
- finish(findStrandedSymbols(r.engine.store).length);
82906
- } catch {
82907
- strandedRefreshing.delete(r.id);
82908
- } finally {
82909
- done();
82910
- }
82911
- });
82912
- });
82913
- } else {
82914
- setImmediate(() => {
82915
- const done = markPass(`stranded-scan:${r.entry.name}`);
82916
- try {
82917
- finish(findStrandedSymbols(r.engine.store).length);
82918
- } catch {
82919
- strandedRefreshing.delete(r.id);
82920
- } finally {
82921
- done();
82922
- }
82923
- });
82924
- }
82925
- }
82926
- return hit?.count ?? -1;
82636
+ const roots = git3(["rev-list", "--max-parents=0", "HEAD"]);
82637
+ const count = git3(["rev-list", "--count", "HEAD"]);
82638
+ const rootCommitSha = roots?.split(/\s+/).filter(Boolean).pop();
82639
+ const commitCount = count && /^\d+$/.test(count) ? Number(count) : void 0;
82640
+ return {
82641
+ ...rootCommitSha && /^[0-9a-f]{40,64}$/i.test(rootCommitSha) ? { rootCommitSha } : {},
82642
+ ...typeof commitCount === "number" ? { commitCount } : {}
82927
82643
  };
82928
- let lastHook = null;
82929
- app.get(
82930
- "/health",
82931
- (c) => c.json({
82932
- ok: true,
82933
- daemonVersion: DAEMON_VERSION,
82934
- lastHook,
82935
- // What the daemon is DOING, not just what it holds. Without these a
82936
- // pegged daemon is only diagnosable by attaching a CPU profiler to the
82937
- // live process — see watch-census.ts for the incident that proved it.
82938
- pass: passState(),
82939
- watch: watchCensus(),
82940
- // Registered but with no engine running (WS-cold-start). Listed so a cold
82941
- // daemon is legible as "nothing has needed waking yet" rather than as
82942
- // "the registry lost my projects" — an invisible cold workspace is
82943
- // indistinguishable from a missing one, and only one of those is fine.
82944
- cold: listWorkspaceEntries().filter((e) => !records.some((r) => normPath(r.root) === normPath(e.path))).map((e) => ({ name: e.name, path: e.path })),
82945
- projects: records.map((r) => ({
82946
- id: r.id,
82947
- name: r.entry.name,
82948
- path: r.root,
82949
- nodes: r.engine.store.nodeCount(),
82950
- // COUNT(*) — same hydration hazard as `/` (HZ-index-hydrate).
82951
- problems: r.engine.store.countNodesByLabel("Problem"),
82952
- solutions: r.engine.store.countNodesByLabel("Solution"),
82953
- stranded: strandedCount(r)
82954
- }))
82955
- })
82956
- );
82957
- const forward = async (c, pathInBody) => {
82958
- const raw2 = await c.req.text();
82959
- let body2 = {};
82960
- try {
82961
- body2 = JSON.parse(raw2);
82962
- } catch {
82963
- return c.json({ error: "bad-json" }, 400);
82964
- }
82965
- lastHook = { at: Date.now(), event: typeof body2["hook_event_name"] === "string" ? body2["hook_event_name"] : c.req.path };
82966
- let owner = ownerOf(records, pathInBody(body2));
82967
- if (!owner) {
82968
- const missPath = pathInBody(body2);
82969
- const registeredRoot = missPath ? pickOwnerRoot(listWorkspaceEntries().map((e) => e.path), missPath) : null;
82970
- const wakeRoot = registeredRoot ?? (missPath ? findGitRoot(missPath) : null);
82971
- if (wakeRoot && attachWorkspace(wakeRoot).attached) {
82972
- owner = ownerOf(records, missPath) ?? null;
82973
- if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
82974
- }
82975
- }
82976
- if (!owner) {
82977
- rootAdopter.noteMiss(pathInBody(body2));
82978
- return c.json({ error: "no-matching-project" }, 404);
82979
- }
82980
- owner.lastHookAt = Date.now();
82981
- const sub = c.req.path;
82982
- const done = markPass(`${sub}:${owner.entry.name}`);
82983
- try {
82984
- return await owner.webApp.fetch(
82985
- new Request(`http://local${sub}`, {
82986
- method: "POST",
82987
- headers: { "content-type": "application/json" },
82988
- body: raw2
82989
- })
82990
- );
82991
- } finally {
82992
- done();
82644
+ }
82645
+ var IGNORE_STARTER = `# .errataignore \u2014 the hard control over what errata may read or send.
82646
+ # Paths, content keywords and tags listed here are dropped from hunks and
82647
+ # narrative before anything leaves this machine, and an ignored path yields no
82648
+ # symbol anchor at all. Scrubbing is best effort; this list is absolute.
82649
+ # A personal list can only add to the organization's list, never weaken it.
82650
+
82651
+ # secrets and environment
82652
+ .env
82653
+ .env.*
82654
+ *.pem
82655
+ *.key
82656
+ *.p12
82657
+ secrets/
82658
+ credentials*
82659
+
82660
+ # fixtures and captured data
82661
+ fixtures/
82662
+ **/testdata/
82663
+ *.dump
82664
+ *.sql.gz
82665
+ `;
82666
+ function applyConsents(profile, config2, c, decidedAt, now = Date.now) {
82667
+ const nextConfig = {
82668
+ ...config2,
82669
+ consent: {
82670
+ ...config2.consent,
82671
+ sync: c.sync !== "off",
82672
+ telemetry: c.telemetry,
82673
+ contributePackages: c.packages && c.sync !== "off"
82674
+ },
82675
+ onboardedAt: config2.onboardedAt ?? now()
82676
+ };
82677
+ const nextProfile = {
82678
+ ...profile,
82679
+ syncConsent: c.sync === "automatic" ? "automatic" : c.sync === "ask" ? "ask-each-time" : "disabled",
82680
+ linkConsents: {
82681
+ structure: c.structure,
82682
+ mineHistory: c.mineHistory,
82683
+ contributeHistory: c.contributeHistory && c.mineHistory && c.sync !== "off",
82684
+ decidedAt
82993
82685
  }
82994
82686
  };
82995
- app.post(
82996
- "/api/hook",
82997
- (c) => forward(c, (b) => {
82998
- const input = b["tool_input"] ?? {};
82999
- const fp = input["file_path"];
83000
- const cwd = b["cwd"];
83001
- return (typeof fp === "string" ? fp : void 0) ?? (typeof cwd === "string" ? cwd : void 0);
83002
- })
83003
- );
83004
- app.post(
83005
- "/api/diagnostics",
83006
- (c) => forward(c, (b) => {
83007
- const loc = b["path"] ?? b["uri"];
83008
- return typeof loc === "string" ? fileUriToFsPath(loc) : void 0;
83009
- })
83010
- );
83011
- const cwdOf = (b) => typeof b["cwd"] === "string" ? b["cwd"] : void 0;
83012
- app.post("/api/turn", (c) => forward(c, cwdOf));
83013
- app.post("/api/message", (c) => forward(c, cwdOf));
83014
- app.post("/api/session-end", (c) => forward(c, cwdOf));
83015
- app.post("/api/context-inject", (c) => forward(c, cwdOf));
83016
- for (const r of records) {
83017
- mountedIds.add(r.id);
83018
- app.route(`/ws/${r.id}`, r.webApp);
83019
- }
83020
- const dispatchHot = async (c) => {
83021
- const id = c.req.param("id");
83022
- const rec = records.find((r) => r.id === id);
83023
- if (!rec) return c.notFound();
83024
- const url2 = new URL(c.req.url);
83025
- url2.pathname = url2.pathname.slice(`/ws/${id}`.length) || "/";
83026
- const method = c.req.method;
83027
- const body2 = method === "GET" || method === "HEAD" ? void 0 : await c.req.raw.arrayBuffer();
83028
- return rec.webApp.fetch(new Request(url2.toString(), { method, headers: c.req.raw.headers, body: body2 }));
82687
+ const prev = profile.linkConsents;
82688
+ const changed = profile.syncConsent !== nextProfile.syncConsent || config2.consent.sync !== nextConfig.consent.sync || config2.consent.telemetry !== nextConfig.consent.telemetry || config2.consent.contributePackages !== nextConfig.consent.contributePackages || !prev || prev.structure !== nextProfile.linkConsents.structure || prev.mineHistory !== nextProfile.linkConsents.mineHistory || prev.contributeHistory !== nextProfile.linkConsents.contributeHistory;
82689
+ return { profile: nextProfile, config: nextConfig, changed };
82690
+ }
82691
+ function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
82692
+ const applied = applyConsents(profile, config2, decision.consents, decision.approvedAt, now);
82693
+ const nextConfig = applied.config;
82694
+ const nextProfile = {
82695
+ ...applied.profile,
82696
+ projectId: decision.projectId,
82697
+ projectLocator: decision.locator
83029
82698
  };
83030
- app.all("/ws/:id", dispatchHot);
83031
- app.all("/ws/:id/*", dispatchHot);
83032
- const retireWorkspace = async (rec, reason) => {
83033
- const idx = records.indexOf(rec);
83034
- if (idx === -1) return;
83035
- records.splice(idx, 1);
83036
- try {
83037
- await rec.engine.stop();
83038
- } catch (err2) {
83039
- console.warn(`[errata] retire ${rec.entry.name}: engine.stop failed \u2014 ${err2 instanceof Error ? err2.message : err2}`);
82699
+ delete nextProfile.projectLinkDisabled;
82700
+ let wroteIgnore = false;
82701
+ if (decision.writeIgnoreStarter !== false) {
82702
+ const target = join34(root, ".errataignore");
82703
+ if (!existsSync29(target)) {
82704
+ writeFileSync22(target, IGNORE_STARTER, "utf8");
82705
+ wroteIgnore = true;
83040
82706
  }
83041
- console.log(
83042
- `[errata] retired ${rec.entry.name} (${reason})` + (reason === "idle" ? " \u2014 idle; the next hook from it re-attaches" : "")
83043
- );
83044
- };
83045
- const sweepIdleWorkspaces = () => {
82707
+ }
82708
+ return { profile: nextProfile, config: nextConfig, wroteIgnore };
82709
+ }
82710
+
82711
+ // src/multi.ts
82712
+ init_registry();
82713
+
82714
+ // src/adopt.ts
82715
+ init_git_remote();
82716
+ import { existsSync as existsSync30 } from "node:fs";
82717
+ import { dirname as dirname12, join as join35 } from "node:path";
82718
+ function findGitRoot(absPath) {
82719
+ let dir = absPath;
82720
+ for (let depth = 0; depth < 64; depth++) {
82721
+ if (existsSync30(join35(dir, ".git"))) return dir;
82722
+ const parent = dirname12(dir);
82723
+ if (parent === dir) return null;
82724
+ dir = parent;
82725
+ }
82726
+ return null;
82727
+ }
82728
+ var RETRY_COOLDOWN_MS = 15 * 6e4;
82729
+ function createRootAdopter(deps) {
82730
+ const attempted = /* @__PURE__ */ new Map();
82731
+ let chain2 = Promise.resolve();
82732
+ const tryAdopt = async (path2) => {
82733
+ if (!deps.linkingAllowed()) return;
82734
+ const root = findGitRoot(path2);
82735
+ if (!root || deps.isKnownRoot(root)) return;
82736
+ const last = attempted.get(root);
83046
82737
  const now = Date.now();
83047
- for (const rec of [...records]) {
83048
- let lastActiveAt = rec.lastHookAt;
83049
- try {
83050
- lastActiveAt = Math.max(lastActiveAt, rec.engine.lastActivityAt());
83051
- } catch {
83052
- }
83053
- const reason = retireDecision({ exists: existsSync30(rec.root), lastActiveAt, now });
83054
- if (reason) void retireWorkspace(rec, reason);
83055
- }
82738
+ if (last !== void 0 && now - last < RETRY_COOLDOWN_MS) return;
82739
+ attempted.set(root, now);
82740
+ if (!resolveGitDir(root)) return;
82741
+ const locator = detectRepoLocator(root);
82742
+ if (!locator) return;
82743
+ const claimed = await deps.lookup(locator);
82744
+ if (!claimed) return;
82745
+ await deps.adopt(root);
82746
+ deps.log(`[errata] adopted ${root} \u2014 ${locator} is claimed by your org (project ${claimed.id})`);
83056
82747
  };
83057
- const idleSweep = setInterval(sweepIdleWorkspaces, WORKSPACE_SWEEP_INTERVAL_MS);
83058
- idleSweep.unref?.();
83059
- let codexWakeFirstPass = true;
83060
- const sweepCodexWake = () => {
83061
- const lookback = codexWakeFirstPass ? CODEX_WAKE_BOOT_LOOKBACK_MS : CODEX_WAKE_LOOKBACK_MS;
83062
- codexWakeFirstPass = false;
83063
- let entries2;
83064
- try {
83065
- entries2 = listWorkspaceEntries();
83066
- } catch {
83067
- return;
83068
- }
83069
- const candidates = codexWakeCandidates({
83070
- entries: entries2,
83071
- liveRoots: new Set(records.map((r) => normPath(r.root))),
83072
- normPath,
83073
- sinceMs: Date.now() - lookback,
83074
- disabled: process.env["ERRATA_CODEX_WAKE_DISABLE"] === "1"
83075
- });
83076
- for (const c of candidates) {
83077
- if (attachWorkspace(c.path).attached) {
83078
- console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini/OpenCode transcript (headless capture)`);
83079
- }
83080
- }
82748
+ return {
82749
+ noteMiss(path2) {
82750
+ if (!path2) return;
82751
+ chain2 = chain2.then(() => tryAdopt(path2)).catch(() => void 0);
82752
+ },
82753
+ idle: () => chain2
83081
82754
  };
83082
- const codexWake = setInterval(sweepCodexWake, WORKSPACE_SWEEP_INTERVAL_MS);
83083
- codexWake.unref?.();
83084
- const codexWakeBoot = setTimeout(sweepCodexWake, 5e3);
83085
- codexWakeBoot.unref?.();
83086
- const reindexAll = async (rOpts) => {
83087
- const out2 = /* @__PURE__ */ new Map();
83088
- const toIndex = records.filter((r) => rOpts?.force || r.engine.store.nodeCount() === 0);
83089
- const plural = (n) => `repositor${n === 1 ? "y" : "ies"}`;
83090
- if (toIndex.length > 0) {
83091
- notifyEvent("index-started", `Indexing ${toIndex.length} ${plural(toIndex.length)}\u2026`);
82755
+ }
82756
+
82757
+ // src/health-sentinel.ts
82758
+ function numEnv4(key, fallback) {
82759
+ const n = Number(process.env[key]);
82760
+ return Number.isFinite(n) && n > 0 ? n : fallback;
82761
+ }
82762
+ var SENTINEL_TICK_MS = numEnv4("ERRATA_SENTINEL_TICK_MS", 3e4);
82763
+ var SENTINEL_WINDOW_TICKS = numEnv4("ERRATA_SENTINEL_WINDOW_TICKS", 6);
82764
+ var SENTINEL_BURN_CORES = numEnv4("ERRATA_SENTINEL_BURN_CORES", 1.2);
82765
+ var SENTINEL_STORM_RATE = numEnv4("ERRATA_SENTINEL_STORM_RATE", 1e3);
82766
+ var SENTINEL_STORM_TICKS = numEnv4("ERRATA_SENTINEL_STORM_TICKS", 4);
82767
+ var SENTINEL_RENOTIFY_MS = numEnv4("ERRATA_SENTINEL_RENOTIFY_MS", 18e5);
82768
+ var SENTINEL_CALM_TICKS = numEnv4("ERRATA_SENTINEL_CALM_TICKS", 2);
82769
+ var SENTINEL_TOAST_COOLDOWN_MS = numEnv4("ERRATA_SENTINEL_TOAST_COOLDOWN_MS", 6e5);
82770
+ function createHealthSentinel(deps = {}) {
82771
+ const now = deps.now ?? Date.now;
82772
+ const cpuUsage = deps.cpuUsage ?? (() => process.cpuUsage());
82773
+ const readCensus = deps.readCensus ?? (() => {
82774
+ const c = watchCensus();
82775
+ return {
82776
+ dispatched: c.dispatched,
82777
+ logged: c.logged,
82778
+ topOffender: c.topOffender ? { path: c.topOffender.path, count: c.topOffender.count } : null
82779
+ };
82780
+ });
82781
+ const activePasses = deps.activePasses ?? (() => passState().active.map((p) => p.name));
82782
+ const rssMb = deps.rssMb ?? (() => Math.round(process.memoryUsage.rss() / 1048576));
82783
+ const toast = deps.notify ?? ((body2, key) => notifyOpsEvent("daemon-distress", body2, { key }));
82784
+ let prevCpu = cpuUsage();
82785
+ let prevTs = now();
82786
+ let prevDispatched = null;
82787
+ let prevLogged = 0;
82788
+ const coreSamples = [];
82789
+ let stormTicks = 0;
82790
+ let calmTicks = 0;
82791
+ let distress = null;
82792
+ let lastNotifiedAt = 0;
82793
+ const lastToastByKind = /* @__PURE__ */ new Map();
82794
+ const attribution = () => {
82795
+ const parts2 = [];
82796
+ const passes = activePasses();
82797
+ if (passes.length > 0) parts2.push(`active pass: ${passes.join(", ")}`);
82798
+ const c = readCensus();
82799
+ if (c.topOffender) parts2.push(`hottest watch path: ${c.topOffender.path}`);
82800
+ parts2.push(`rss ${rssMb()}MB`);
82801
+ return parts2.join(" \xB7 ");
82802
+ };
82803
+ const maybeToast = (d, t) => {
82804
+ if (t - (lastToastByKind.get(d.kind) ?? 0) < SENTINEL_TOAST_COOLDOWN_MS) return;
82805
+ lastToastByKind.set(d.kind, t);
82806
+ toast(d.headline, d.kind);
82807
+ lastNotifiedAt = t;
82808
+ };
82809
+ const transition = (next) => {
82810
+ const t = now();
82811
+ if (next && !distress) {
82812
+ distress = next;
82813
+ deps.ledger?.("enter", next);
82814
+ maybeToast(next, t);
82815
+ deps.onChange?.(next);
82816
+ } else if (next && distress) {
82817
+ distress.body = next.body;
82818
+ distress.headline = next.headline;
82819
+ if (t - lastNotifiedAt >= SENTINEL_RENOTIFY_MS) maybeToast(distress, t);
82820
+ } else if (!next && distress) {
82821
+ deps.ledger?.("clear", distress);
82822
+ distress = null;
82823
+ deps.onChange?.(null);
83092
82824
  }
83093
- let indexed = 0;
83094
- for (const r of records) {
83095
- if (!rOpts?.force && r.engine.store.nodeCount() > 0) {
83096
- out2.set(r.id, r.engine.store.nodeCount());
83097
- continue;
83098
- }
83099
- const done = markPass(`reindex:${r.entry.name}`);
83100
- try {
83101
- const idx = await r.engine.reindex({ skipEmbed: rOpts?.skipEmbed ?? true, quiet: true });
83102
- out2.set(r.id, idx.nodes);
83103
- indexed++;
83104
- console.log(`[errata] indexed ${r.entry.name}: ${idx.nodes} nodes / ${idx.edges} edges`);
83105
- } catch (err2) {
83106
- console.warn(`[errata] index failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83107
- out2.set(r.id, r.engine.store.nodeCount());
83108
- } finally {
83109
- done();
82825
+ };
82826
+ const evaluate = () => {
82827
+ const t = now();
82828
+ const wallMs = Math.max(1, t - prevTs);
82829
+ const cpu = cpuUsage();
82830
+ const cores = (cpu.user + cpu.system - prevCpu.user - prevCpu.system) / 1e3 / wallMs;
82831
+ prevCpu = cpu;
82832
+ prevTs = t;
82833
+ coreSamples.push(cores);
82834
+ if (coreSamples.length > SENTINEL_WINDOW_TICKS) coreSamples.shift();
82835
+ const c = readCensus();
82836
+ const dispatchRate = prevDispatched === null ? 0 : (c.dispatched - prevDispatched) / wallMs * 1e3;
82837
+ const survived = c.logged - prevLogged;
82838
+ prevDispatched = c.dispatched;
82839
+ prevLogged = c.logged;
82840
+ stormTicks = dispatchRate > SENTINEL_STORM_RATE && survived === 0 ? stormTicks + 1 : 0;
82841
+ let wanted = null;
82842
+ if (stormTicks >= SENTINEL_STORM_TICKS) {
82843
+ wanted = {
82844
+ kind: "watch-storm",
82845
+ headline: `errata's file watcher is stuck busy (~${Math.round(dispatchRate)} events/sec with nothing changing) \u2014 run \`errata status\` for detail`,
82846
+ body: `filesystem watcher is processing ~${Math.round(dispatchRate)} events/sec with NOTHING surviving \u2014 a wedged or ignored-tree watch is burning CPU for no signal \xB7 ${attribution()}`,
82847
+ sinceTs: distress?.kind === "watch-storm" ? distress.sinceTs : t
82848
+ };
82849
+ } else {
82850
+ const windowFull = coreSamples.length >= SENTINEL_WINDOW_TICKS;
82851
+ const avgCores = coreSamples.reduce((a, b) => a + b, 0) / Math.max(1, coreSamples.length);
82852
+ if (windowFull && avgCores >= SENTINEL_BURN_CORES && cores >= SENTINEL_BURN_CORES && activePasses().length === 0) {
82853
+ wanted = {
82854
+ kind: "cpu-burn",
82855
+ headline: `errata is using more CPU than expected (${avgCores.toFixed(1)} cores with no work running) \u2014 run \`errata status\` for detail`,
82856
+ body: `sustained CPU with no pass running \u2014 avg ${avgCores.toFixed(1)} cores \xB7 ${attribution()}`,
82857
+ sinceTs: distress?.kind === "cpu-burn" ? distress.sinceTs : t
82858
+ };
83110
82859
  }
83111
82860
  }
83112
- if (toIndex.length > 0) {
83113
- notifyEvent("index-completed", `Indexed ${indexed} ${plural(indexed)}`);
82861
+ if (wanted) {
82862
+ calmTicks = 0;
82863
+ transition(wanted);
82864
+ } else if (distress) {
82865
+ calmTicks++;
82866
+ if (calmTicks >= SENTINEL_CALM_TICKS) {
82867
+ calmTicks = 0;
82868
+ transition(null);
82869
+ }
83114
82870
  }
83115
- return out2;
83116
82871
  };
83117
- const { server, port } = await listenServer(app.fetch, opts.webPort ?? 7891);
83118
- baseUrl = `http://127.0.0.1:${port}`;
83119
- try {
83120
- ensureDir(globalDir());
83121
- writeFileSync22(
83122
- lockPath,
83123
- JSON.stringify({ pid: process.pid, webUiUrl: baseUrl, startedAt: Date.now() }),
83124
- "utf8"
83125
- );
83126
- } catch {
83127
- }
83128
- for (const r of records) {
82872
+ const interval = deps.scheduleTicks === false ? null : setInterval(() => {
83129
82873
  try {
83130
- writeFileSync22(
83131
- r.engine.paths.daemonLock,
83132
- JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${r.id}`, startedAt: Date.now() }),
83133
- "utf8"
83134
- );
83135
- registerWorkspace(r.engine.profile, r.root);
82874
+ evaluate();
83136
82875
  } catch {
83137
82876
  }
83138
- }
83139
- if (opts.reindexOnStart !== false && records.length > 0) {
83140
- void reindexAll({ skipEmbed: opts.skipEmbed ?? true });
83141
- }
83142
- const rootAdopter = createRootAdopter({
83143
- isKnownRoot: (root) => pickOwnerRoot(records.map((r) => r.root), root) !== null,
83144
- linkingAllowed: () => {
83145
- const c = loadConfig();
83146
- return c.consent.sync && hasCloudCredential(c);
83147
- },
83148
- lookup: async (locator) => cloudNow().lookupProject(locator),
83149
- adopt: async (root) => {
83150
- let profile = loadProfile(root);
83151
- if (!profile) {
83152
- profile = autodetectProfile(root);
83153
- saveProfile(root, profile);
83154
- }
83155
- registerWorkspace(profile, root);
83156
- attachWorkspace(root);
83157
- },
83158
- // eslint-disable-next-line no-console
83159
- log: (line) => console.log(line)
83160
- });
83161
- const ambientLinkAll = async () => {
82877
+ }, SENTINEL_TICK_MS);
82878
+ interval?.unref?.();
82879
+ return {
82880
+ current: () => distress ? { ...distress } : null,
82881
+ tick: evaluate,
82882
+ stop: () => {
82883
+ if (interval) clearInterval(interval);
82884
+ }
82885
+ };
82886
+ }
82887
+
82888
+ // src/codex-wake.ts
82889
+ var CODEX_WAKE_LOOKBACK_MS = 10 * 6e4;
82890
+ var CODEX_WAKE_BOOT_LOOKBACK_MS = 24 * 60 * 6e4;
82891
+ function codexWakeCandidates(input) {
82892
+ if (input.disabled) return [];
82893
+ const find = input.find ?? ((cwd, since) => codexRolloutsForCwd(cwd, { sinceMs: since, limit: 1 }).length + geminiTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length + opencodeTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length);
82894
+ const out2 = [];
82895
+ for (const entry of input.entries) {
82896
+ if (input.liveRoots.has(input.normPath(entry.path))) continue;
82897
+ let n = 0;
82898
+ try {
82899
+ n = find(entry.path, input.sinceMs);
82900
+ } catch {
82901
+ continue;
82902
+ }
82903
+ if (n > 0) out2.push({ name: entry.name, path: entry.path });
82904
+ }
82905
+ return out2;
82906
+ }
82907
+
82908
+ // src/multi.ts
82909
+ var projectSymbolSalts = /* @__PURE__ */ new Map();
82910
+ async function resolveProjectSymbolSalt(client, projectId) {
82911
+ const cached2 = projectSymbolSalts.get(projectId);
82912
+ if (cached2) return { projectId, salt: cached2 };
82913
+ const { salt } = await client.projectSymbolSalt(projectId);
82914
+ projectSymbolSalts.set(projectId, salt);
82915
+ return { projectId, salt };
82916
+ }
82917
+ function normPath(p) {
82918
+ return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
82919
+ }
82920
+ function contextKey(p) {
82921
+ return `${[...p.stack].sort().join(",")}|${p.domains[0] ?? p.languages[0] ?? "?"}`;
82922
+ }
82923
+ function pickOwnerRoot(roots, absPath) {
82924
+ if (!absPath) return null;
82925
+ const n = normPath(absPath);
82926
+ let best = null;
82927
+ let bestLen = -1;
82928
+ for (const root of roots) {
82929
+ const r = normPath(root);
82930
+ if ((n === r || n.startsWith(r + "/")) && r.length > bestLen) {
82931
+ best = root;
82932
+ bestLen = r.length;
82933
+ }
82934
+ }
82935
+ return best;
82936
+ }
82937
+ function ownerOf(records, absPath) {
82938
+ const root = pickOwnerRoot(records.map((r) => r.root), absPath);
82939
+ return root == null ? null : records.find((r) => r.root === root) ?? null;
82940
+ }
82941
+ async function startMultiDaemon(opts = {}) {
82942
+ const lockPath = globalDaemonLock();
82943
+ if (opts.enforceSingleton !== false && isDaemonAlive(lockPath)) {
82944
+ throw new Error(
82945
+ `an errata daemon is already running (lock: ${lockPath}). Run \`errata stop\` first, or attach to the running one.`
82946
+ );
82947
+ }
82948
+ const cfg = loadConfig();
82949
+ const CLOUD_TIMEOUT_MS = 12e4;
82950
+ const initialCloudUrl = normalizeCloudUrl(opts.cloudUrl ?? cfg.cloudUrl);
82951
+ const initialCfg = { ...cfg, cloudUrl: initialCloudUrl };
82952
+ if (opts.apiKey) {
82953
+ initialCfg.apiKey = opts.apiKey;
82954
+ initialCfg.accessToken = null;
82955
+ initialCfg.refreshToken = null;
82956
+ initialCfg.tokenEndpoint = null;
82957
+ initialCfg.oauthClientId = null;
82958
+ }
82959
+ const cloud = authedCloudClient(initialCfg, { timeoutMs: CLOUD_TIMEOUT_MS, daemonVersion: DAEMON_VERSION });
82960
+ const cloudNow = () => {
83162
82961
  const c = loadConfig();
83163
- if (!c.consent.sync || !hasCloudCredential(c)) return;
83164
- const client = cloudNow();
83165
- for (const r of records) {
83166
- const out2 = await ensureProjectLink(r.root, r.engine.profile, client);
83167
- if (out2.linked) {
83168
- console.log(
83169
- `[errata] linked ${r.engine.profile.name} \u2192 project ${out2.projectId}${out2.created ? " (created)" : ""}`
83170
- );
83171
- } else if (out2.reason === "error") {
83172
- console.warn(`[errata] project link failed for ${r.engine.profile.name}: ${out2.detail}`);
83173
- }
83174
- const hyd = await hydrateProject({
83175
- root: r.root,
83176
- profile: r.engine.profile,
83177
- store: r.engine.store,
83178
- cloud: client
82962
+ const baseUrl2 = normalizeCloudUrl(opts.cloudUrl ?? c.cloudUrl);
82963
+ const current = { ...c, cloudUrl: baseUrl2 };
82964
+ if (opts.apiKey) {
82965
+ current.apiKey = opts.apiKey;
82966
+ current.accessToken = null;
82967
+ current.refreshToken = null;
82968
+ current.tokenEndpoint = null;
82969
+ current.oauthClientId = null;
82970
+ }
82971
+ return authedCloudClient(current, { timeoutMs: CLOUD_TIMEOUT_MS, daemonVersion: DAEMON_VERSION });
82972
+ };
82973
+ const identityFor = (entry) => {
82974
+ if (opts.apiKey) {
82975
+ return {
82976
+ profileName: null,
82977
+ profile: null,
82978
+ rule: "env-config-path",
82979
+ invalid: false,
82980
+ reason: "daemon started with an explicit --api-key (process-pinned)"
82981
+ };
82982
+ }
82983
+ return resolveWorkspaceIdentity(loadConfig(), entry);
82984
+ };
82985
+ const identityBlocks = /* @__PURE__ */ new Map();
82986
+ const noteIdentityBlock = (wsId, wsName, identity) => {
82987
+ if (!identityBlocks.has(wsId)) {
82988
+ identityBlocks.set(wsId, {
82989
+ workspace: wsName,
82990
+ profileName: identity.profileName,
82991
+ reason: identity.reason,
82992
+ since: Date.now()
83179
82993
  });
83180
- if (hyd.hydrated) {
83181
- r.engine.markContextDirty();
83182
- console.log(
83183
- `[errata] primed ${r.engine.profile.name} from project \u2014 ${hyd.nodes} priors, ${hyd.edges} links`
83184
- );
83185
- } else if (hyd.reason === "error") {
83186
- console.warn(`[errata] project prime failed for ${r.engine.profile.name}: ${hyd.detail}`);
83187
- }
83188
- if (r.engine.profile.projectId) {
83189
- try {
83190
- const { salt } = await resolveProjectSymbolSalt(client, r.engine.profile.projectId);
83191
- const re = await reanchorProject({
83192
- root: r.root,
83193
- profile: r.engine.profile,
83194
- store: r.engine.store,
83195
- cloud: client,
83196
- salt
83197
- });
83198
- if (re.reanchored) {
83199
- r.engine.markContextDirty();
83200
- console.log(
83201
- `[errata] reanchored ${r.engine.profile.name} \u2014 ${re.anchors} anchors onto local code (${re.dropped} unresolved, dropped)`
83202
- );
83203
- } else if (re.reason === "error") {
83204
- console.warn(`[errata] project reanchor failed for ${r.engine.profile.name}: ${re.detail}`);
83205
- }
83206
- if (r.engine.profile.linkConsents && r.engine.profile.projectHydratedAt && !historyInFlight.has(r.root)) {
83207
- historyInFlight.add(r.root);
83208
- const engine = r.engine;
83209
- const root = r.root;
83210
- const anchorIndex = {
83211
- nodesByRelPath: (paths) => {
83212
- if (!records.some((x) => x.root === root)) {
83213
- throw new Error("workspace detached mid-walk; the watermark holds and the next pass resumes");
83214
- }
83215
- return engine.store.nodesByRelPath(paths);
83216
- }
83217
- };
83218
- void startHistoryStage(root, engine.profile, client, {
83219
- anchors: { index: anchorIndex, salt },
83220
- saveProfile: (p) => {
83221
- Object.assign(engine.profile, p);
83222
- saveProfile(root, engine.profile);
83223
- },
83224
- log: (line) => console.warn(line)
83225
- }).then((stage2) => {
83226
- if (stage2.profile !== engine.profile) {
83227
- Object.assign(engine.profile, stage2.profile);
83228
- saveProfile(root, engine.profile);
83229
- }
83230
- const state = engine.profile.historyStage?.state ?? "consent-pending";
83231
- const c2 = engine.profile.historyStage?.counts;
83232
- console.log(
83233
- `[errata] history ${state} for ${engine.profile.name}` + (c2 ? ` \xB7 ${c2.commitsWalked ?? 0} commits walked \xB7 ${c2.changesExtracted ?? 0} changes extracted \xB7 ${c2.pairs ?? 0} pairs \xB7 ${c2.claims ?? 0} claims \xB7 ${c2.anchors ?? 0} anchors \xB7 ${c2.packages ?? 0} packages` : "") + (engine.profile.historyStage?.lastError ? ` \xB7 ${engine.profile.historyStage.lastError}` : "")
83234
- );
83235
- }).catch((err2) => {
83236
- console.warn(`[errata] history walk failed for ${engine.profile.name}: ${err2 instanceof Error ? err2.message : err2}`);
83237
- }).finally(() => historyInFlight.delete(root));
83238
- }
83239
- } catch {
83240
- }
83241
- }
82994
+ console.error(`[identity] HOLDING cloud traffic for workspace "${wsName}": ${identity.reason}`);
83242
82995
  }
83243
82996
  };
83244
- const historyInFlight = /* @__PURE__ */ new Set();
83245
- void ambientLinkAll();
83246
- const packageAutoAll = async () => {
83247
- const out2 = /* @__PURE__ */ new Map();
83248
- for (const r of records) {
83249
- await new Promise((resolve7) => setImmediate(resolve7));
83250
- const done = markPass(`package-index:${r.entry.name}`);
83251
- try {
83252
- out2.set(
83253
- r.id,
83254
- runLockfilePass({
83255
- root: r.root,
83256
- store: r.engine.store,
83257
- workspaceId: r.id,
83258
- languages: r.engine.profile.languages,
83259
- log: (m) => console.log(m)
83260
- })
83261
- );
83262
- } catch (err2) {
83263
- console.warn(`[errata] package index failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83264
- } finally {
83265
- done();
83266
- }
82997
+ const clearIdentityBlock = (wsId) => {
82998
+ if (identityBlocks.delete(wsId)) console.log(`[identity] workspace unblocked \u2014 spooled capture drains on this pass`);
82999
+ };
83000
+ const projectHolds = /* @__PURE__ */ new Map();
83001
+ const noteProjectHold = (wsId, wsName, reason) => {
83002
+ if (!projectHolds.has(wsId)) {
83003
+ projectHolds.set(wsId, { workspace: wsName, reason, since: Date.now() });
83004
+ console.error(
83005
+ `[project] HOLDING cloud push for workspace "${wsName}": cannot prove its project \u2014 ${reason}. Shipping without the assertion would publish project-scoped knowledge to the whole org; the batch stays pending instead.`
83006
+ );
83267
83007
  }
83268
- return out2;
83269
83008
  };
83270
- const daemon = {
83271
- url: baseUrl,
83272
- records,
83273
- sharedStore,
83274
- attachWorkspace,
83275
- reindexAll,
83276
- packageAutoAll,
83277
- async tickAll() {
83278
- const out2 = /* @__PURE__ */ new Map();
83279
- for (const r of records) {
83280
- const done = markPass(`tick:${r.entry.name}`);
83281
- try {
83282
- out2.set(r.id, await r.engine.tick());
83283
- } catch (err2) {
83284
- console.warn(`[errata] tick failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83285
- } finally {
83286
- done();
83287
- }
83288
- }
83289
- return out2;
83290
- },
83291
- semanticMaintenanceAll() {
83292
- const out2 = /* @__PURE__ */ new Map();
83293
- for (const r of records) {
83294
- const done = markPass(`semantic:${r.entry.name}`);
83295
- try {
83296
- out2.set(r.id, r.engine.semanticMaintenance());
83297
- } catch (err2) {
83298
- console.warn(
83299
- `[errata] semantic maintenance failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
83300
- );
83301
- } finally {
83302
- done();
83303
- }
83009
+ const clearProjectHold = (wsId) => {
83010
+ if (projectHolds.delete(wsId)) console.log(`[project] project assertion restored \u2014 held contributions drain on this pass`);
83011
+ };
83012
+ const cloudForIdentity = (identity) => {
83013
+ if (identity.invalid) return blockedCloudClient(identity.reason);
83014
+ if (identity.rule === "workspace-binding" && identity.profileName) {
83015
+ return authedCloudClientForProfile(loadConfig(), identity.profileName, {
83016
+ timeoutMs: CLOUD_TIMEOUT_MS,
83017
+ daemonVersion: DAEMON_VERSION
83018
+ });
83019
+ }
83020
+ return cloudNow();
83021
+ };
83022
+ const sharedDbPath = sharedStorePath();
83023
+ ensureParent(sharedDbPath);
83024
+ const sharedStore = openGraphStore({ path: sharedDbPath });
83025
+ const bundledRun = import.meta.url.endsWith(".mjs");
83026
+ const consolidateWorker = bundledRun && sharedDbPath !== ":memory:" ? new ConsolidateWorker() : null;
83027
+ startLoopLagMonitor();
83028
+ let baseUrl = "";
83029
+ const records = [];
83030
+ const mountedIds = /* @__PURE__ */ new Set();
83031
+ const machineDominantLanguage = () => {
83032
+ const langCounts = /* @__PURE__ */ new Map();
83033
+ for (const r of records) {
83034
+ for (const l of r.engine.profile.languages ?? []) {
83035
+ const k = String(l).trim().toLowerCase();
83036
+ if (k) langCounts.set(k, (langCounts.get(k) ?? 0) + 1);
83304
83037
  }
83305
- return out2;
83306
- },
83307
- async nightlyFor(ids) {
83308
- const want = new Set(ids);
83309
- const out2 = /* @__PURE__ */ new Map();
83310
- for (const r of records) {
83311
- if (!want.has(r.id)) continue;
83312
- const done = markPass(`nightly:${r.entry.name}`);
83313
- try {
83314
- out2.set(r.id, await r.engine.nightly());
83315
- } catch (err2) {
83316
- console.warn(`[errata] nightly failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83317
- } finally {
83318
- done();
83319
- }
83038
+ }
83039
+ return [...langCounts.entries()].sort(
83040
+ (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)
83041
+ )[0]?.[0];
83042
+ };
83043
+ const updatePoller = opts.updateCheck ? startUpdatePoller({
83044
+ channel: loadConfig().updateChannel,
83045
+ onChange: (pending) => {
83046
+ if (pending) {
83047
+ console.log(`\u26A1 errata update available: ${pending.current} \u2192 ${pending.latest} \u2014 run: errata update`);
83320
83048
  }
83321
- return out2;
83322
- },
83323
- async nightlyAll() {
83324
- return daemon.nightlyFor(records.map((r) => r.id));
83049
+ for (const r of records) r.engine.markContextDirty();
83050
+ }
83051
+ }) : null;
83052
+ const healthSentinel = createHealthSentinel({
83053
+ onChange: () => {
83054
+ for (const r of records) r.engine.markContextDirty();
83325
83055
  },
83326
- async percolateAll() {
83327
- const ts = Date.now();
83328
- const out2 = /* @__PURE__ */ new Map();
83329
- const repoBasename = (locator) => (locator.split("/").filter(Boolean).pop() ?? locator).toLowerCase();
83330
- for (const r of records) {
83331
- const profile = r.engine.profile;
83332
- const aliases = profile.repoLocatorAliases ?? [];
83333
- const locator = profile.repoLocator;
83334
- if (!locator || aliases.length === 0) continue;
83335
- try {
83336
- for (const alias of aliases) {
83337
- if (repoBasename(alias) !== repoBasename(locator)) {
83338
- console.log(
83339
- `[errata] project alias NOT auto-migrated for ${r.entry.name} (${alias} \u2192 ${locator}: repo name differs \u2014 a fork/repoint, not a rename). Run scripts/migrate-project-alias.ts if this really is the same repo.`
83340
- );
83341
- continue;
83342
- }
83343
- const m = migrateProjectAlias(sharedStore, { from: alias, to: locator, ts });
83344
- if (m.nodesRewritten || m.edgesRewritten) {
83345
- console.log(
83346
- `[errata] project alias migrated for ${r.entry.name}: ${alias} \u2192 ${locator} (${m.nodesRewritten} nodes, ${m.edgesRewritten} edges)`
83347
- );
83348
- }
83349
- }
83350
- delete profile.repoLocatorAliases;
83351
- const onDisk = loadProfile(r.root);
83352
- if (onDisk) {
83353
- delete onDisk.repoLocatorAliases;
83354
- saveProfile(r.root, onDisk);
83355
- }
83356
- } catch (err2) {
83357
- console.warn(`[errata] project-alias migration failed for ${r.entry.name} (kept for next pass): ${err2 instanceof Error ? err2.message : err2}`);
83358
- }
83056
+ ledger: (event, d) => {
83057
+ const dir = records[0]?.engine.paths.configDir;
83058
+ if (dir) {
83059
+ appendPassLedger(dir, "health-sentinel", 0, {
83060
+ [`distress:${event}:${d.kind}`]: 1,
83061
+ distressSinceTs: d.sinceTs
83062
+ });
83359
83063
  }
83360
- let inlineRecords = records;
83361
- if (consolidateWorker) {
83362
- const fileBacked = records.filter((r) => r.engine.paths.castalia !== ":memory:");
83363
- if (fileBacked.length > 0) {
83364
- try {
83365
- const res = await consolidateWorker.run({
83366
- kind: "percolateAll",
83367
- sharedDbPath,
83368
- ts,
83369
- workspaces: fileBacked.map((r) => ({
83370
- recordId: r.id,
83371
- // Repo-keyed when linked: two checkouts of one repo are ONE
83372
- // project in observedInProjects, not two independent
83373
- // observations (PJ-local — the Q9 independence gate).
83374
- workspaceId: r.engine.profile.repoLocator ?? r.engine.profile.id,
83375
- castaliaPath: r.engine.paths.castalia
83376
- }))
83377
- });
83378
- for (const [id, v] of res) out2.set(id, v);
83379
- inlineRecords = records.filter((r) => r.engine.paths.castalia === ":memory:");
83380
- } catch (err2) {
83381
- console.warn(`[errata] percolate worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
83382
- }
83383
- }
83064
+ }
83065
+ });
83066
+ let serverPending = null;
83067
+ const boundaryListeners = [];
83068
+ let boundaryFlushing = false;
83069
+ let settleRunning = false;
83070
+ let boundaryFlushStartedAt = 0;
83071
+ let boundaryFlushStage = "";
83072
+ let boundaryFlushUnits = 0;
83073
+ let boundaryFlushLastAdvanceAt = 0;
83074
+ let boundaryFlushBatches = 0;
83075
+ const beginFlush = (stage2) => {
83076
+ boundaryFlushing = true;
83077
+ boundaryFlushStartedAt = Date.now();
83078
+ boundaryFlushLastAdvanceAt = Date.now();
83079
+ boundaryFlushUnits = 0;
83080
+ boundaryFlushBatches = 0;
83081
+ boundaryFlushItems = 0;
83082
+ boundaryFlushDetail = "";
83083
+ boundaryFlushStage = stage2;
83084
+ };
83085
+ const flushStage = (stage2) => {
83086
+ boundaryFlushStage = stage2;
83087
+ };
83088
+ let boundaryFlushItems = 0;
83089
+ let boundaryFlushDetail = "";
83090
+ const noteFlushProgress = (units, items = 0, detail = "") => {
83091
+ boundaryFlushBatches++;
83092
+ boundaryFlushUnits += Math.max(0, units);
83093
+ boundaryFlushItems += Math.max(0, items);
83094
+ if (detail) boundaryFlushDetail = detail;
83095
+ if (units > 0 || items > 0) boundaryFlushLastAdvanceAt = Date.now();
83096
+ };
83097
+ const flushSnapshot = () => ({
83098
+ running: boundaryFlushing,
83099
+ stage: boundaryFlushStage,
83100
+ detail: boundaryFlushDetail,
83101
+ ageMs: boundaryFlushStartedAt ? Date.now() - boundaryFlushStartedAt : 0,
83102
+ units: boundaryFlushUnits,
83103
+ items: boundaryFlushItems,
83104
+ chunks: boundaryFlushBatches,
83105
+ stalledMs: boundaryFlushLastAdvanceAt ? Date.now() - boundaryFlushLastAdvanceAt : 0,
83106
+ // Held workspaces, so a hold is REPORTABLE and not merely logged once at the
83107
+ // transition. `errata status` reads this rather than the on-disk profile:
83108
+ // the profile records what the workspace INTENDS to assert, which is why
83109
+ // status could print "→ project 019f462d" for hours while every batch was
83110
+ // being held or shipped unlinked. A hold is the daemon's actual behavior.
83111
+ holds: [
83112
+ ...[...projectHolds.values()].map((h) => ({
83113
+ workspace: h.workspace,
83114
+ kind: "project",
83115
+ reason: h.reason,
83116
+ sinceMs: Date.now() - h.since
83117
+ })),
83118
+ ...[...identityBlocks.values()].map((h) => ({
83119
+ workspace: h.workspace,
83120
+ kind: "identity",
83121
+ reason: h.reason,
83122
+ sinceMs: Date.now() - h.since
83123
+ }))
83124
+ ]
83125
+ });
83126
+ const endFlush = () => {
83127
+ boundaryFlushing = false;
83128
+ boundaryFlushStage = "";
83129
+ };
83130
+ const fireBoundary = () => {
83131
+ for (const l of boundaryListeners) {
83132
+ try {
83133
+ l();
83134
+ } catch {
83384
83135
  }
83385
- for (const r of inlineRecords) {
83386
- const done = markPass(`percolate-inline:${r.entry.name}`);
83387
- try {
83388
- out2.set(
83389
- r.id,
83390
- percolate(r.engine.store, sharedStore, {
83391
- workspaceId: r.engine.profile.repoLocator ?? r.engine.profile.id,
83392
- ts
83393
- })
83394
- );
83395
- } catch (err2) {
83396
- console.warn(
83397
- `[errata] percolate failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
83398
- );
83399
- } finally {
83400
- done();
83401
- }
83136
+ }
83137
+ };
83138
+ const buildRecord = (entry) => {
83139
+ const id = entry.id;
83140
+ const wsIdentity = identityFor(entry);
83141
+ let wsCloud = cloud;
83142
+ if (wsIdentity.rule === "workspace-binding") {
83143
+ if (wsIdentity.invalid) {
83144
+ noteIdentityBlock(entry.id, entry.name, wsIdentity);
83145
+ wsCloud = blockedCloudClient(wsIdentity.reason);
83146
+ } else {
83147
+ wsCloud = authedCloudClientForProfile(loadConfig(), wsIdentity.profileName, {
83148
+ timeoutMs: CLOUD_TIMEOUT_MS,
83149
+ daemonVersion: DAEMON_VERSION
83150
+ });
83151
+ console.log(`[identity] workspace "${entry.name}" bound \u2192 profile "${wsIdentity.profileName}"`);
83402
83152
  }
83403
- return out2;
83404
- },
83405
- async induceL2Abstractions(touched) {
83406
- const ctxByWs = /* @__PURE__ */ new Map();
83407
- for (const r of records) ctxByWs.set(r.engine.profile.id, contextKey(r.engine.profile));
83408
- const ts = Date.now();
83409
- if (consolidateWorker) {
83410
- try {
83411
- return await consolidateWorker.run({
83412
- kind: "induceAbstractions",
83413
- sharedDbPath,
83414
- ts,
83415
- ctxByWs: Object.fromEntries(ctxByWs),
83416
- originMachine: machineId(),
83417
- ...touched ? { touched: new Set(touched) } : {}
83418
- });
83419
- } catch (err2) {
83420
- console.warn(`[errata] abstraction worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
83421
- }
83153
+ }
83154
+ const engine = createWorkspaceEngine({
83155
+ workspaceRoot: entry.path,
83156
+ cloud: wsCloud,
83157
+ apiKey: opts.apiKey,
83158
+ reviewUrl: () => `${baseUrl}/ws/${id}/review`,
83159
+ sharedStore,
83160
+ pendingUpdate: () => serverPending ?? updatePoller?.current() ?? null,
83161
+ // Ops-gated (default OFF): the 🚨 notice reads as "errata broke my
83162
+ // machine" to a non-operator. Detection + ledger run regardless — only
83163
+ // this push surface (and the toast, gated in notifyOpsEvent) is scoped.
83164
+ daemonDistress: () => loadConfig().opsNotices ? healthSentinel.current() : null,
83165
+ ...opts.skipWatchers ? { skipWatchers: true } : {}
83166
+ });
83167
+ const webApp = buildWebUi({
83168
+ paths: engine.paths,
83169
+ store: engine.store,
83170
+ log: engine.log,
83171
+ profile: engine.profile,
83172
+ daemonVersion: DAEMON_VERSION,
83173
+ basePath: `/ws/${id}`,
83174
+ onWorkingFileChanged: engine.webHooks.onWorkingFileChanged,
83175
+ onHook: engine.webHooks.onHook,
83176
+ onHookEvent: engine.webHooks.onHookEvent,
83177
+ onShellEvent: engine.webHooks.onShellEvent,
83178
+ // The tool-run RECORD seam (SP-tool-ground). The single-engine path wired
83179
+ // this in #1172; this sibling builder never did, so on every multi
83180
+ // deployment (i.e. every real one) the webui logged "tool-run recorded"
83181
+ // and dropped the record on the floor — toolSessionsTracked read 0 across
83182
+ // 936 turns while the log showed records landing (VS-corroboration-rate).
83183
+ onToolsObserved: engine.webHooks.onToolsObserved,
83184
+ onToolError: engine.webHooks.onToolError,
83185
+ onDiagnostics: engine.webHooks.onDiagnostics,
83186
+ onTurn: engine.webHooks.onTurn,
83187
+ onMessage: engine.webHooks.onMessage,
83188
+ onSessionEnd: (e) => {
83189
+ engine.webHooks.onSessionEnd(e);
83190
+ fireBoundary();
83422
83191
  }
83423
- return induceAbstractions(sharedStore, {
83424
- contextOf: (ws) => ctxByWs.get(ws) ?? null,
83425
- ts,
83426
- // Source-tag minted principles to this machine so they stay attributable
83427
- // across the user's machines once L3 sync lands (P4 / §8.6).
83428
- originMachine: machineId(),
83429
- ...touched ? { touched } : {}
83430
- });
83431
- },
83432
- async induceL2Triage() {
83433
- const ctxByWs = /* @__PURE__ */ new Map();
83434
- for (const r of records) {
83435
- const p = r.engine.profile;
83436
- ctxByWs.set(p.id, {
83437
- features: { stack: [...p.stack].sort().join(","), domain: p.domains[0] ?? p.languages[0] ?? "" }
83438
- });
83192
+ });
83193
+ return { entry, engine, webApp, root: entry.path, id, lastHookAt: Date.now() };
83194
+ };
83195
+ const pruned = pruneMissingWorkspaces();
83196
+ if (pruned.length > 0) {
83197
+ console.log(`[errata] pruned ${pruned.length} registered workspace(s) whose path no longer exists`);
83198
+ }
83199
+ if (registryCorrupt()) {
83200
+ console.error("[identity] workspaces.json is UNREADABLE \u2014 all per-workspace cloud push will hold until it parses (fail-closed)");
83201
+ } else {
83202
+ for (const b of boundWorkspaceEntries()) {
83203
+ console.log(`[identity] bound workspace: ${b.name} \u2192 profile "${b.installationProfile}"`);
83204
+ }
83205
+ }
83206
+ const entries = listWorkspaceEntries();
83207
+ if (opts.eagerWorkspaces ?? EAGER_WORKSPACES_AT_BOOT) {
83208
+ for (const entry of entries) {
83209
+ try {
83210
+ records.push(buildRecord(entry));
83211
+ } catch (err2) {
83212
+ console.warn(`[errata] skipping ${entry.name} (${entry.path}): ${err2 instanceof Error ? err2.message : err2}`);
83439
83213
  }
83440
- const ts = Date.now();
83441
- if (consolidateWorker) {
83442
- try {
83443
- return await consolidateWorker.run({
83444
- kind: "induceTriage",
83445
- sharedDbPath,
83446
- ts,
83447
- triageCtxByWs: Object.fromEntries(ctxByWs)
83214
+ }
83215
+ if (entries.length > 0 && records.length < entries.length) {
83216
+ const failed = entries.length - records.length;
83217
+ console.warn(
83218
+ `[errata] \u26A0 ${failed} of ${entries.length} project(s) failed to start \u2014 likely a stale daemon holding DB locks. Run \`errata stop\` (or kill the old errata process) and retry.`
83219
+ );
83220
+ }
83221
+ } else if (entries.length > 0) {
83222
+ console.log(
83223
+ `[errata] ${entries.length} workspace(s) registered \xB7 cold \u2014 an engine spawns on the first hook from each`
83224
+ );
83225
+ }
83226
+ const app = new Hono2();
83227
+ const attachWorkspace = (root) => {
83228
+ const norm = normPath(root);
83229
+ if (records.some((r) => normPath(r.root) === norm)) return { attached: false, id: null, reason: "already-watched" };
83230
+ const entry = listWorkspaceEntries().find((e) => normPath(e.path) === norm);
83231
+ if (!entry) return { attached: false, id: null, reason: "not-registered" };
83232
+ let rec;
83233
+ try {
83234
+ rec = buildRecord(entry);
83235
+ } catch (err2) {
83236
+ return { attached: false, id: null, reason: err2 instanceof Error ? err2.message : "build-failed" };
83237
+ }
83238
+ records.push(rec);
83239
+ void ambientLinkAll();
83240
+ try {
83241
+ writeFileSync23(
83242
+ rec.engine.paths.daemonLock,
83243
+ JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${rec.id}`, startedAt: Date.now() }),
83244
+ "utf8"
83245
+ );
83246
+ registerWorkspace(rec.engine.profile, rec.root);
83247
+ } catch {
83248
+ }
83249
+ void rec.engine.reindex({ skipEmbed: true, quiet: rec.engine.store.nodeCount() > 0 }).catch(() => {
83250
+ });
83251
+ console.log(`[errata] hot-registered ${rec.entry.name} (${rec.id}) \u2014 now watching live`);
83252
+ return { attached: true, id: rec.id };
83253
+ };
83254
+ app.post("/api/attach", async (c) => {
83255
+ let body2 = {};
83256
+ try {
83257
+ body2 = await c.req.json();
83258
+ } catch {
83259
+ return c.json({ error: "bad-json" }, 400);
83260
+ }
83261
+ const root = typeof body2["path"] === "string" ? body2["path"] : null;
83262
+ if (!root) return c.json({ error: "path-required" }, 400);
83263
+ return c.json(attachWorkspace(root));
83264
+ });
83265
+ app.post("/api/notify-test", (c) => {
83266
+ notify("Errata \xB7 test", "Fired from the daemon \u2014 if you see this, daemon toasts render.");
83267
+ return c.json({ ok: true });
83268
+ });
83269
+ app.get(
83270
+ "/",
83271
+ (c) => c.json({
83272
+ daemonVersion: DAEMON_VERSION,
83273
+ pendingUpdate: updatePoller?.current() ?? null,
83274
+ projects: records.map((r) => ({
83275
+ id: r.id,
83276
+ name: r.entry.name,
83277
+ path: r.root,
83278
+ stack: r.entry.stack,
83279
+ nodes: r.engine.store.nodeCount(),
83280
+ // COUNT(*) — the materializing findNodesByLabel(...).length form held
83281
+ // the event loop for seconds per poll once stores grew (HZ-index-hydrate).
83282
+ problems: r.engine.store.countNodesByLabel("Problem"),
83283
+ solutions: r.engine.store.countNodesByLabel("Solution"),
83284
+ endpoints: `/ws/${r.id}/`
83285
+ })),
83286
+ humanView: "run `errata report` \u2014 the dashboard was retired (GRAFT 4e)"
83287
+ })
83288
+ );
83289
+ const STRANDED_TTL_MS = 6e4;
83290
+ const strandedCache = /* @__PURE__ */ new Map();
83291
+ const strandedRefreshing = /* @__PURE__ */ new Set();
83292
+ const strandedCount = (r) => {
83293
+ const hit = strandedCache.get(r.id);
83294
+ if (hit && Date.now() - hit.at < STRANDED_TTL_MS) return hit.count;
83295
+ if (!strandedRefreshing.has(r.id)) {
83296
+ strandedRefreshing.add(r.id);
83297
+ const finish = (count) => {
83298
+ strandedCache.set(r.id, { at: Date.now(), count });
83299
+ strandedRefreshing.delete(r.id);
83300
+ };
83301
+ if (consolidateWorker && r.engine.paths.castalia !== ":memory:") {
83302
+ consolidateWorker.run({ kind: "strandedCount", sharedDbPath, castaliaPath: r.engine.paths.castalia }).then((n) => finish(Number(n))).catch(() => {
83303
+ setImmediate(() => {
83304
+ const done = markPass(`stranded-scan:${r.entry.name}`);
83305
+ try {
83306
+ finish(findStrandedSymbols(r.engine.store).length);
83307
+ } catch {
83308
+ strandedRefreshing.delete(r.id);
83309
+ } finally {
83310
+ done();
83311
+ }
83448
83312
  });
83449
- } catch (err2) {
83450
- console.warn(`[errata] triage-induction worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
83451
- }
83452
- }
83453
- return induceTriage(sharedStore, {
83454
- contextOf: (ws) => ctxByWs.get(ws) ?? null,
83455
- ts
83456
- });
83457
- },
83458
- async revisitStaleTriageRoutes() {
83459
- if (consolidateWorker) {
83460
- try {
83461
- return await consolidateWorker.run({ kind: "revisitTriage", sharedDbPath, ts: Date.now() });
83462
- } catch {
83463
- }
83464
- }
83465
- return revisitStaleRoutes(sharedStore, Date.now());
83466
- },
83467
- async revisitStalePrinciples() {
83468
- if (consolidateWorker) {
83469
- try {
83470
- return await consolidateWorker.run({ kind: "revisitPrinciples", sharedDbPath, ts: Date.now() });
83471
- } catch {
83472
- }
83313
+ });
83314
+ } else {
83315
+ setImmediate(() => {
83316
+ const done = markPass(`stranded-scan:${r.entry.name}`);
83317
+ try {
83318
+ finish(findStrandedSymbols(r.engine.store).length);
83319
+ } catch {
83320
+ strandedRefreshing.delete(r.id);
83321
+ } finally {
83322
+ done();
83323
+ }
83324
+ });
83473
83325
  }
83474
- return revisitContradictedPrinciples(sharedStore, Date.now());
83475
- },
83476
- async syncPrinciplesPublic() {
83477
- if (!loadConfig().consent.sync) return { uploaded: 0, skipped: "consent-off" };
83478
- const ignore2 = loadClaimIgnorePatterns(globalDir());
83479
- const primaryLanguage = machineDominantLanguage();
83480
- const payload = buildPrincipleIngest(sharedStore, DAEMON_VERSION, ignore2, 2, {
83481
- ...primaryLanguage ? { primaryLanguage } : {}
83482
- });
83483
- if (!payload) return { uploaded: 0 };
83484
- const res = await cloudNow().ingest(payload);
83485
- return { uploaded: res.accepted };
83486
- },
83487
- async syncTriagePublic() {
83488
- const cfg2 = loadConfig();
83489
- if (!cfg2.consent.sync) return { uploaded: 0, skipped: "consent-off" };
83490
- const ignore2 = loadClaimIgnorePatterns(globalDir());
83491
- const primaryLanguage = machineDominantLanguage();
83492
- const languages = records.flatMap((r) => r.engine.profile.languages ?? []);
83493
- const packages = cfg2.consent.contributePackages ? records.flatMap(
83494
- (r) => r.engine.store.findNodesByLabel("Package").map((p) => ({
83495
- name: String(p.attrs["name"] ?? ""),
83496
- purl: String(p.attrs["purl"] ?? p.id)
83497
- }))
83498
- ) : [];
83499
- const tokenAnchors = buildBucketTokenAnchors(languages, packages);
83500
- const payload = buildTriageIngest(sharedStore, DAEMON_VERSION, ignore2, 2, {
83326
+ }
83327
+ return hit?.count ?? -1;
83328
+ };
83329
+ let lastHook = null;
83330
+ app.get(
83331
+ "/health",
83332
+ (c) => c.json({
83333
+ ok: true,
83334
+ daemonVersion: DAEMON_VERSION,
83335
+ lastHook,
83336
+ // What the daemon is DOING, not just what it holds. Without these a
83337
+ // pegged daemon is only diagnosable by attaching a CPU profiler to the
83338
+ // live process — see watch-census.ts for the incident that proved it.
83339
+ pass: passState(),
83340
+ watch: watchCensus(),
83341
+ // Registered but with no engine running (WS-cold-start). Listed so a cold
83342
+ // daemon is legible as "nothing has needed waking yet" rather than as
83343
+ // "the registry lost my projects" — an invisible cold workspace is
83344
+ // indistinguishable from a missing one, and only one of those is fine.
83345
+ cold: listWorkspaceEntries().filter((e) => !records.some((r) => normPath(r.root) === normPath(e.path))).map((e) => ({ name: e.name, path: e.path })),
83346
+ projects: records.map((r) => ({
83347
+ id: r.id,
83348
+ name: r.entry.name,
83349
+ path: r.root,
83350
+ nodes: r.engine.store.nodeCount(),
83351
+ // COUNT(*) — same hydration hazard as `/` (HZ-index-hydrate).
83352
+ problems: r.engine.store.countNodesByLabel("Problem"),
83353
+ solutions: r.engine.store.countNodesByLabel("Solution"),
83354
+ stranded: strandedCount(r)
83355
+ }))
83356
+ })
83357
+ );
83358
+ const forward = async (c, pathInBody) => {
83359
+ const raw2 = await c.req.text();
83360
+ let body2 = {};
83361
+ try {
83362
+ body2 = JSON.parse(raw2);
83363
+ } catch {
83364
+ return c.json({ error: "bad-json" }, 400);
83365
+ }
83366
+ lastHook = { at: Date.now(), event: typeof body2["hook_event_name"] === "string" ? body2["hook_event_name"] : c.req.path };
83367
+ let owner = ownerOf(records, pathInBody(body2));
83368
+ if (!owner) {
83369
+ const missPath = pathInBody(body2);
83370
+ const registeredRoot = missPath ? pickOwnerRoot(listWorkspaceEntries().map((e) => e.path), missPath) : null;
83371
+ const wakeRoot = registeredRoot ?? (missPath ? findGitRoot(missPath) : null);
83372
+ if (wakeRoot && attachWorkspace(wakeRoot).attached) {
83373
+ owner = ownerOf(records, missPath) ?? null;
83374
+ if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
83375
+ }
83376
+ }
83377
+ if (!owner) {
83378
+ rootAdopter.noteMiss(pathInBody(body2));
83379
+ return c.json({ error: "no-matching-project" }, 404);
83380
+ }
83381
+ owner.lastHookAt = Date.now();
83382
+ const sub = c.req.path;
83383
+ const done = markPass(`${sub}:${owner.entry.name}`);
83384
+ try {
83385
+ return await owner.webApp.fetch(
83386
+ new Request(`http://local${sub}`, {
83387
+ method: "POST",
83388
+ headers: { "content-type": "application/json" },
83389
+ body: raw2
83390
+ })
83391
+ );
83392
+ } finally {
83393
+ done();
83394
+ }
83395
+ };
83396
+ app.post(
83397
+ "/api/hook",
83398
+ (c) => forward(c, (b) => {
83399
+ const input = b["tool_input"] ?? {};
83400
+ const fp = input["file_path"];
83401
+ const cwd = b["cwd"];
83402
+ return (typeof fp === "string" ? fp : void 0) ?? (typeof cwd === "string" ? cwd : void 0);
83403
+ })
83404
+ );
83405
+ app.post(
83406
+ "/api/diagnostics",
83407
+ (c) => forward(c, (b) => {
83408
+ const loc = b["path"] ?? b["uri"];
83409
+ return typeof loc === "string" ? fileUriToFsPath(loc) : void 0;
83410
+ })
83411
+ );
83412
+ const cwdOf = (b) => typeof b["cwd"] === "string" ? b["cwd"] : void 0;
83413
+ app.post("/api/turn", (c) => forward(c, cwdOf));
83414
+ app.post("/api/message", (c) => forward(c, cwdOf));
83415
+ app.post("/api/session-end", (c) => forward(c, cwdOf));
83416
+ app.post("/api/context-inject", (c) => forward(c, cwdOf));
83417
+ for (const r of records) {
83418
+ mountedIds.add(r.id);
83419
+ app.route(`/ws/${r.id}`, r.webApp);
83420
+ }
83421
+ const dispatchHot = async (c) => {
83422
+ const id = c.req.param("id");
83423
+ const rec = records.find((r) => r.id === id);
83424
+ if (!rec) return c.notFound();
83425
+ const url2 = new URL(c.req.url);
83426
+ url2.pathname = url2.pathname.slice(`/ws/${id}`.length) || "/";
83427
+ const method = c.req.method;
83428
+ const body2 = method === "GET" || method === "HEAD" ? void 0 : await c.req.raw.arrayBuffer();
83429
+ return rec.webApp.fetch(new Request(url2.toString(), { method, headers: c.req.raw.headers, body: body2 }));
83430
+ };
83431
+ app.all("/ws/:id", dispatchHot);
83432
+ app.all("/ws/:id/*", dispatchHot);
83433
+ const retireWorkspace = async (rec, reason) => {
83434
+ const idx = records.indexOf(rec);
83435
+ if (idx === -1) return;
83436
+ records.splice(idx, 1);
83437
+ try {
83438
+ await rec.engine.stop();
83439
+ } catch (err2) {
83440
+ console.warn(`[errata] retire ${rec.entry.name}: engine.stop failed \u2014 ${err2 instanceof Error ? err2.message : err2}`);
83441
+ }
83442
+ console.log(
83443
+ `[errata] retired ${rec.entry.name} (${reason})` + (reason === "idle" ? " \u2014 idle; the next hook from it re-attaches" : "")
83444
+ );
83445
+ };
83446
+ const sweepIdleWorkspaces = () => {
83447
+ const now = Date.now();
83448
+ for (const rec of [...records]) {
83449
+ let lastActiveAt = rec.lastHookAt;
83450
+ try {
83451
+ lastActiveAt = Math.max(lastActiveAt, rec.engine.lastActivityAt());
83452
+ } catch {
83453
+ }
83454
+ const reason = retireDecision({ exists: existsSync31(rec.root), lastActiveAt, now });
83455
+ if (reason) void retireWorkspace(rec, reason);
83456
+ }
83457
+ };
83458
+ const idleSweep = setInterval(sweepIdleWorkspaces, WORKSPACE_SWEEP_INTERVAL_MS);
83459
+ idleSweep.unref?.();
83460
+ let codexWakeFirstPass = true;
83461
+ const sweepCodexWake = () => {
83462
+ const lookback = codexWakeFirstPass ? CODEX_WAKE_BOOT_LOOKBACK_MS : CODEX_WAKE_LOOKBACK_MS;
83463
+ codexWakeFirstPass = false;
83464
+ let entries2;
83465
+ try {
83466
+ entries2 = listWorkspaceEntries();
83467
+ } catch {
83468
+ return;
83469
+ }
83470
+ const candidates = codexWakeCandidates({
83471
+ entries: entries2,
83472
+ liveRoots: new Set(records.map((r) => normPath(r.root))),
83473
+ normPath,
83474
+ sinceMs: Date.now() - lookback,
83475
+ disabled: process.env["ERRATA_CODEX_WAKE_DISABLE"] === "1"
83476
+ });
83477
+ for (const c of candidates) {
83478
+ if (attachWorkspace(c.path).attached) {
83479
+ console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini/OpenCode transcript (headless capture)`);
83480
+ }
83481
+ }
83482
+ };
83483
+ const codexWake = setInterval(sweepCodexWake, WORKSPACE_SWEEP_INTERVAL_MS);
83484
+ codexWake.unref?.();
83485
+ const codexWakeBoot = setTimeout(sweepCodexWake, 5e3);
83486
+ codexWakeBoot.unref?.();
83487
+ const reindexAll = async (rOpts) => {
83488
+ const out2 = /* @__PURE__ */ new Map();
83489
+ const toIndex = records.filter((r) => rOpts?.force || r.engine.store.nodeCount() === 0);
83490
+ const plural = (n) => `repositor${n === 1 ? "y" : "ies"}`;
83491
+ if (toIndex.length > 0) {
83492
+ notifyEvent("index-started", `Indexing ${toIndex.length} ${plural(toIndex.length)}\u2026`);
83493
+ }
83494
+ let indexed = 0;
83495
+ for (const r of records) {
83496
+ if (!rOpts?.force && r.engine.store.nodeCount() > 0) {
83497
+ out2.set(r.id, r.engine.store.nodeCount());
83498
+ continue;
83499
+ }
83500
+ const done = markPass(`reindex:${r.entry.name}`);
83501
+ try {
83502
+ const idx = await r.engine.reindex({ skipEmbed: rOpts?.skipEmbed ?? true, quiet: true });
83503
+ out2.set(r.id, idx.nodes);
83504
+ indexed++;
83505
+ console.log(`[errata] indexed ${r.entry.name}: ${idx.nodes} nodes / ${idx.edges} edges`);
83506
+ } catch (err2) {
83507
+ console.warn(`[errata] index failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83508
+ out2.set(r.id, r.engine.store.nodeCount());
83509
+ } finally {
83510
+ done();
83511
+ }
83512
+ }
83513
+ if (toIndex.length > 0) {
83514
+ notifyEvent("index-completed", `Indexed ${indexed} ${plural(indexed)}`);
83515
+ }
83516
+ return out2;
83517
+ };
83518
+ const { server, port } = await listenServer(app.fetch, opts.webPort ?? 7891);
83519
+ baseUrl = `http://127.0.0.1:${port}`;
83520
+ try {
83521
+ ensureDir(globalDir());
83522
+ writeFileSync23(
83523
+ lockPath,
83524
+ JSON.stringify({ pid: process.pid, webUiUrl: baseUrl, startedAt: Date.now() }),
83525
+ "utf8"
83526
+ );
83527
+ } catch {
83528
+ }
83529
+ for (const r of records) {
83530
+ try {
83531
+ writeFileSync23(
83532
+ r.engine.paths.daemonLock,
83533
+ JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${r.id}`, startedAt: Date.now() }),
83534
+ "utf8"
83535
+ );
83536
+ registerWorkspace(r.engine.profile, r.root);
83537
+ } catch {
83538
+ }
83539
+ }
83540
+ if (opts.reindexOnStart !== false && records.length > 0) {
83541
+ void reindexAll({ skipEmbed: opts.skipEmbed ?? true });
83542
+ }
83543
+ const rootAdopter = createRootAdopter({
83544
+ isKnownRoot: (root) => pickOwnerRoot(records.map((r) => r.root), root) !== null,
83545
+ linkingAllowed: () => {
83546
+ const c = loadConfig();
83547
+ return c.consent.sync && hasCloudCredential(c);
83548
+ },
83549
+ lookup: async (locator) => cloudNow().lookupProject(locator),
83550
+ adopt: async (root) => {
83551
+ let profile = loadProfile(root);
83552
+ if (!profile) {
83553
+ profile = autodetectProfile(root);
83554
+ saveProfile(root, profile);
83555
+ }
83556
+ registerWorkspace(profile, root);
83557
+ attachWorkspace(root);
83558
+ },
83559
+ // eslint-disable-next-line no-console
83560
+ log: (line) => console.log(line)
83561
+ });
83562
+ const ambientLinkAll = async () => {
83563
+ const c = loadConfig();
83564
+ if (!c.consent.sync || !hasCloudCredential(c)) return;
83565
+ const client = cloudNow();
83566
+ for (const r of records) {
83567
+ const out2 = await ensureProjectLink(r.root, r.engine.profile, client);
83568
+ if (out2.linked) {
83569
+ console.log(
83570
+ `[errata] linked ${r.engine.profile.name} \u2192 project ${out2.projectId}${out2.created ? " (created)" : ""}`
83571
+ );
83572
+ } else if (out2.reason === "error") {
83573
+ console.warn(`[errata] project link failed for ${r.engine.profile.name}: ${out2.detail}`);
83574
+ }
83575
+ const hyd = await hydrateProject({
83576
+ root: r.root,
83577
+ profile: r.engine.profile,
83578
+ store: r.engine.store,
83579
+ cloud: client
83580
+ });
83581
+ if (hyd.hydrated) {
83582
+ r.engine.markContextDirty();
83583
+ console.log(
83584
+ `[errata] primed ${r.engine.profile.name} from project \u2014 ${hyd.nodes} priors, ${hyd.edges} links`
83585
+ );
83586
+ } else if (hyd.reason === "error") {
83587
+ console.warn(`[errata] project prime failed for ${r.engine.profile.name}: ${hyd.detail}`);
83588
+ }
83589
+ if (r.engine.profile.projectId) {
83590
+ try {
83591
+ const { salt } = await resolveProjectSymbolSalt(client, r.engine.profile.projectId);
83592
+ const re = await reanchorProject({
83593
+ root: r.root,
83594
+ profile: r.engine.profile,
83595
+ store: r.engine.store,
83596
+ cloud: client,
83597
+ salt
83598
+ });
83599
+ if (re.reanchored) {
83600
+ r.engine.markContextDirty();
83601
+ console.log(
83602
+ `[errata] reanchored ${r.engine.profile.name} \u2014 ${re.anchors} anchors onto local code (${re.dropped} unresolved, dropped)`
83603
+ );
83604
+ } else if (re.reason === "error") {
83605
+ console.warn(`[errata] project reanchor failed for ${r.engine.profile.name}: ${re.detail}`);
83606
+ }
83607
+ if (r.engine.profile.projectHydratedAt && !historyInFlight.has(r.root)) {
83608
+ historyInFlight.add(r.root);
83609
+ const engine = r.engine;
83610
+ const root = r.root;
83611
+ const anchorIndex = {
83612
+ nodesByRelPath: (paths) => {
83613
+ if (!records.some((x) => x.root === root)) {
83614
+ throw new Error("workspace detached mid-walk; the watermark holds and the next pass resumes");
83615
+ }
83616
+ return engine.store.nodesByRelPath(paths);
83617
+ }
83618
+ };
83619
+ void startHistoryStage(root, engine.profile, client, {
83620
+ anchors: { index: anchorIndex, salt },
83621
+ // PS-web projects: the console's consents (Privacy tab) win over
83622
+ // the profile's memory; applied to config and profile here, with
83623
+ // the change printed so a flipped switch is visible in the log.
83624
+ applyConsents: (p, consents) => {
83625
+ const cfg2 = loadConfig();
83626
+ const applied = applyConsents(p, cfg2, consents, (/* @__PURE__ */ new Date()).toISOString());
83627
+ if (applied.changed) {
83628
+ saveConfig(applied.config);
83629
+ Object.assign(engine.profile, applied.profile);
83630
+ saveProfile(root, engine.profile);
83631
+ console.log(
83632
+ `[errata] consents applied from the console for ${engine.profile.name}: sync=${consents.sync} telemetry=${consents.telemetry ? "on" : "off"} packages=${consents.packages ? "on" : "off"} structure=${consents.structure ? "on" : "off"} history=${consents.mineHistory ? consents.contributeHistory ? "mine+contribute" : "mine" : "off"}`
83633
+ );
83634
+ }
83635
+ return applied.profile;
83636
+ },
83637
+ saveProfile: (p) => {
83638
+ Object.assign(engine.profile, p);
83639
+ saveProfile(root, engine.profile);
83640
+ },
83641
+ log: (line) => console.warn(line)
83642
+ }).then((stage2) => {
83643
+ if (stage2.profile !== engine.profile) {
83644
+ Object.assign(engine.profile, stage2.profile);
83645
+ saveProfile(root, engine.profile);
83646
+ }
83647
+ const state = engine.profile.historyStage?.state ?? "consent-pending";
83648
+ const c2 = engine.profile.historyStage?.counts;
83649
+ console.log(
83650
+ `[errata] history ${state} for ${engine.profile.name}` + (c2 ? ` \xB7 ${c2.commitsWalked ?? 0} commits walked \xB7 ${c2.changesExtracted ?? 0} changes extracted \xB7 ${c2.pairs ?? 0} pairs \xB7 ${c2.claims ?? 0} claims \xB7 ${c2.anchors ?? 0} anchors \xB7 ${c2.packages ?? 0} packages` : "") + (engine.profile.historyStage?.lastError ? ` \xB7 ${engine.profile.historyStage.lastError}` : "")
83651
+ );
83652
+ }).catch((err2) => {
83653
+ console.warn(`[errata] history walk failed for ${engine.profile.name}: ${err2 instanceof Error ? err2.message : err2}`);
83654
+ }).finally(() => historyInFlight.delete(root));
83655
+ }
83656
+ } catch {
83657
+ }
83658
+ }
83659
+ }
83660
+ };
83661
+ const historyInFlight = /* @__PURE__ */ new Set();
83662
+ void ambientLinkAll();
83663
+ const packageAutoAll = async () => {
83664
+ const out2 = /* @__PURE__ */ new Map();
83665
+ for (const r of records) {
83666
+ await new Promise((resolve7) => setImmediate(resolve7));
83667
+ const done = markPass(`package-index:${r.entry.name}`);
83668
+ try {
83669
+ out2.set(
83670
+ r.id,
83671
+ runLockfilePass({
83672
+ root: r.root,
83673
+ store: r.engine.store,
83674
+ workspaceId: r.id,
83675
+ languages: r.engine.profile.languages,
83676
+ log: (m) => console.log(m)
83677
+ })
83678
+ );
83679
+ } catch (err2) {
83680
+ console.warn(`[errata] package index failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83681
+ } finally {
83682
+ done();
83683
+ }
83684
+ }
83685
+ return out2;
83686
+ };
83687
+ const daemon = {
83688
+ url: baseUrl,
83689
+ records,
83690
+ sharedStore,
83691
+ attachWorkspace,
83692
+ reindexAll,
83693
+ packageAutoAll,
83694
+ async tickAll() {
83695
+ const out2 = /* @__PURE__ */ new Map();
83696
+ for (const r of records) {
83697
+ const done = markPass(`tick:${r.entry.name}`);
83698
+ try {
83699
+ out2.set(r.id, await r.engine.tick());
83700
+ } catch (err2) {
83701
+ console.warn(`[errata] tick failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83702
+ } finally {
83703
+ done();
83704
+ }
83705
+ }
83706
+ return out2;
83707
+ },
83708
+ semanticMaintenanceAll() {
83709
+ const out2 = /* @__PURE__ */ new Map();
83710
+ for (const r of records) {
83711
+ const done = markPass(`semantic:${r.entry.name}`);
83712
+ try {
83713
+ out2.set(r.id, r.engine.semanticMaintenance());
83714
+ } catch (err2) {
83715
+ console.warn(
83716
+ `[errata] semantic maintenance failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
83717
+ );
83718
+ } finally {
83719
+ done();
83720
+ }
83721
+ }
83722
+ return out2;
83723
+ },
83724
+ async nightlyFor(ids) {
83725
+ const want = new Set(ids);
83726
+ const out2 = /* @__PURE__ */ new Map();
83727
+ for (const r of records) {
83728
+ if (!want.has(r.id)) continue;
83729
+ const done = markPass(`nightly:${r.entry.name}`);
83730
+ try {
83731
+ out2.set(r.id, await r.engine.nightly());
83732
+ } catch (err2) {
83733
+ console.warn(`[errata] nightly failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
83734
+ } finally {
83735
+ done();
83736
+ }
83737
+ }
83738
+ return out2;
83739
+ },
83740
+ async nightlyAll() {
83741
+ return daemon.nightlyFor(records.map((r) => r.id));
83742
+ },
83743
+ async percolateAll() {
83744
+ const ts = Date.now();
83745
+ const out2 = /* @__PURE__ */ new Map();
83746
+ const repoBasename = (locator) => (locator.split("/").filter(Boolean).pop() ?? locator).toLowerCase();
83747
+ for (const r of records) {
83748
+ const profile = r.engine.profile;
83749
+ const aliases = profile.repoLocatorAliases ?? [];
83750
+ const locator = profile.repoLocator;
83751
+ if (!locator || aliases.length === 0) continue;
83752
+ try {
83753
+ for (const alias of aliases) {
83754
+ if (repoBasename(alias) !== repoBasename(locator)) {
83755
+ console.log(
83756
+ `[errata] project alias NOT auto-migrated for ${r.entry.name} (${alias} \u2192 ${locator}: repo name differs \u2014 a fork/repoint, not a rename). Run scripts/migrate-project-alias.ts if this really is the same repo.`
83757
+ );
83758
+ continue;
83759
+ }
83760
+ const m = migrateProjectAlias(sharedStore, { from: alias, to: locator, ts });
83761
+ if (m.nodesRewritten || m.edgesRewritten) {
83762
+ console.log(
83763
+ `[errata] project alias migrated for ${r.entry.name}: ${alias} \u2192 ${locator} (${m.nodesRewritten} nodes, ${m.edgesRewritten} edges)`
83764
+ );
83765
+ }
83766
+ }
83767
+ delete profile.repoLocatorAliases;
83768
+ const onDisk = loadProfile(r.root);
83769
+ if (onDisk) {
83770
+ delete onDisk.repoLocatorAliases;
83771
+ saveProfile(r.root, onDisk);
83772
+ }
83773
+ } catch (err2) {
83774
+ console.warn(`[errata] project-alias migration failed for ${r.entry.name} (kept for next pass): ${err2 instanceof Error ? err2.message : err2}`);
83775
+ }
83776
+ }
83777
+ let inlineRecords = records;
83778
+ if (consolidateWorker) {
83779
+ const fileBacked = records.filter((r) => r.engine.paths.castalia !== ":memory:");
83780
+ if (fileBacked.length > 0) {
83781
+ try {
83782
+ const res = await consolidateWorker.run({
83783
+ kind: "percolateAll",
83784
+ sharedDbPath,
83785
+ ts,
83786
+ workspaces: fileBacked.map((r) => ({
83787
+ recordId: r.id,
83788
+ // Repo-keyed when linked: two checkouts of one repo are ONE
83789
+ // project in observedInProjects, not two independent
83790
+ // observations (PJ-local — the Q9 independence gate).
83791
+ workspaceId: r.engine.profile.repoLocator ?? r.engine.profile.id,
83792
+ castaliaPath: r.engine.paths.castalia
83793
+ }))
83794
+ });
83795
+ for (const [id, v] of res) out2.set(id, v);
83796
+ inlineRecords = records.filter((r) => r.engine.paths.castalia === ":memory:");
83797
+ } catch (err2) {
83798
+ console.warn(`[errata] percolate worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
83799
+ }
83800
+ }
83801
+ }
83802
+ for (const r of inlineRecords) {
83803
+ const done = markPass(`percolate-inline:${r.entry.name}`);
83804
+ try {
83805
+ out2.set(
83806
+ r.id,
83807
+ percolate(r.engine.store, sharedStore, {
83808
+ workspaceId: r.engine.profile.repoLocator ?? r.engine.profile.id,
83809
+ ts
83810
+ })
83811
+ );
83812
+ } catch (err2) {
83813
+ console.warn(
83814
+ `[errata] percolate failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
83815
+ );
83816
+ } finally {
83817
+ done();
83818
+ }
83819
+ }
83820
+ return out2;
83821
+ },
83822
+ async induceL2Abstractions(touched) {
83823
+ const ctxByWs = /* @__PURE__ */ new Map();
83824
+ for (const r of records) ctxByWs.set(r.engine.profile.id, contextKey(r.engine.profile));
83825
+ const ts = Date.now();
83826
+ if (consolidateWorker) {
83827
+ try {
83828
+ return await consolidateWorker.run({
83829
+ kind: "induceAbstractions",
83830
+ sharedDbPath,
83831
+ ts,
83832
+ ctxByWs: Object.fromEntries(ctxByWs),
83833
+ originMachine: machineId(),
83834
+ ...touched ? { touched: new Set(touched) } : {}
83835
+ });
83836
+ } catch (err2) {
83837
+ console.warn(`[errata] abstraction worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
83838
+ }
83839
+ }
83840
+ return induceAbstractions(sharedStore, {
83841
+ contextOf: (ws) => ctxByWs.get(ws) ?? null,
83842
+ ts,
83843
+ // Source-tag minted principles to this machine so they stay attributable
83844
+ // across the user's machines once L3 sync lands (P4 / §8.6).
83845
+ originMachine: machineId(),
83846
+ ...touched ? { touched } : {}
83847
+ });
83848
+ },
83849
+ async induceL2Triage() {
83850
+ const ctxByWs = /* @__PURE__ */ new Map();
83851
+ for (const r of records) {
83852
+ const p = r.engine.profile;
83853
+ ctxByWs.set(p.id, {
83854
+ features: { stack: [...p.stack].sort().join(","), domain: p.domains[0] ?? p.languages[0] ?? "" }
83855
+ });
83856
+ }
83857
+ const ts = Date.now();
83858
+ if (consolidateWorker) {
83859
+ try {
83860
+ return await consolidateWorker.run({
83861
+ kind: "induceTriage",
83862
+ sharedDbPath,
83863
+ ts,
83864
+ triageCtxByWs: Object.fromEntries(ctxByWs)
83865
+ });
83866
+ } catch (err2) {
83867
+ console.warn(`[errata] triage-induction worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
83868
+ }
83869
+ }
83870
+ return induceTriage(sharedStore, {
83871
+ contextOf: (ws) => ctxByWs.get(ws) ?? null,
83872
+ ts
83873
+ });
83874
+ },
83875
+ async revisitStaleTriageRoutes() {
83876
+ if (consolidateWorker) {
83877
+ try {
83878
+ return await consolidateWorker.run({ kind: "revisitTriage", sharedDbPath, ts: Date.now() });
83879
+ } catch {
83880
+ }
83881
+ }
83882
+ return revisitStaleRoutes(sharedStore, Date.now());
83883
+ },
83884
+ async revisitStalePrinciples() {
83885
+ if (consolidateWorker) {
83886
+ try {
83887
+ return await consolidateWorker.run({ kind: "revisitPrinciples", sharedDbPath, ts: Date.now() });
83888
+ } catch {
83889
+ }
83890
+ }
83891
+ return revisitContradictedPrinciples(sharedStore, Date.now());
83892
+ },
83893
+ async syncPrinciplesPublic() {
83894
+ if (!loadConfig().consent.sync) return { uploaded: 0, skipped: "consent-off" };
83895
+ const ignore2 = loadClaimIgnorePatterns(globalDir());
83896
+ const primaryLanguage = machineDominantLanguage();
83897
+ const payload = buildPrincipleIngest(sharedStore, DAEMON_VERSION, ignore2, 2, {
83898
+ ...primaryLanguage ? { primaryLanguage } : {}
83899
+ });
83900
+ if (!payload) return { uploaded: 0 };
83901
+ const res = await cloudNow().ingest(payload);
83902
+ return { uploaded: res.accepted };
83903
+ },
83904
+ async syncTriagePublic() {
83905
+ const cfg2 = loadConfig();
83906
+ if (!cfg2.consent.sync) return { uploaded: 0, skipped: "consent-off" };
83907
+ const ignore2 = loadClaimIgnorePatterns(globalDir());
83908
+ const primaryLanguage = machineDominantLanguage();
83909
+ const languages = records.flatMap((r) => r.engine.profile.languages ?? []);
83910
+ const packages = cfg2.consent.contributePackages ? records.flatMap(
83911
+ (r) => r.engine.store.findNodesByLabel("Package").map((p) => ({
83912
+ name: String(p.attrs["name"] ?? ""),
83913
+ purl: String(p.attrs["purl"] ?? p.id)
83914
+ }))
83915
+ ) : [];
83916
+ const tokenAnchors = buildBucketTokenAnchors(languages, packages);
83917
+ const payload = buildTriageIngest(sharedStore, DAEMON_VERSION, ignore2, 2, {
83501
83918
  ...primaryLanguage ? { primaryLanguage } : {},
83502
83919
  tokenAnchors,
83503
83920
  // Scoped bucket keys carry package names — same consent as the purls.
@@ -83733,660 +84150,274 @@ async function startMultiDaemon(opts = {}) {
83733
84150
  // failing every pass. Same total, different bug.
83734
84151
  blockedRepeat,
83735
84152
  rejectedRepeat
83736
- });
83737
- return res.accepted;
83738
- });
83739
- }
83740
- }
83741
- return { uploaded, ...errors.length > 0 ? { errors } : {} };
83742
- },
83743
- async backfillCausalEdges() {
83744
- if (!loadConfig().consent.sync) return { accepted: 0, rejected: 0, skipped: "consent-off" };
83745
- const client = cloudNow();
83746
- let accepted = 0;
83747
- let rejected = 0;
83748
- const errors = [];
83749
- for (const r of records) {
83750
- const payload = buildCausalEdgeBackfill(r.engine.store, r.engine.profile, DAEMON_VERSION);
83751
- if (!payload) continue;
83752
- try {
83753
- const res = await client.ingest(payload);
83754
- accepted += res.accepted;
83755
- rejected += res.rejected;
83756
- if (res.rejected > 0) errors.push(...res.violations.slice(0, 5));
83757
- } catch (err2) {
83758
- errors.push(
83759
- `[${r.engine.profile.name ?? r.engine.profile.id}] backfill: ${err2 instanceof Error ? err2.message : String(err2)}`
83760
- );
83761
- }
83762
- }
83763
- return { accepted, rejected, ...errors.length > 0 ? { errors } : {} };
83764
- },
83765
- async pullTriagePublic() {
83766
- if (!loadConfig().consent.sync) return { merged: 0, skipped: "consent-off" };
83767
- const stack = /* @__PURE__ */ new Set();
83768
- const domains = /* @__PURE__ */ new Set();
83769
- for (const r of records) {
83770
- for (const s of r.engine.profile.stack) stack.add(s);
83771
- for (const d of r.engine.profile.domains) domains.add(d);
83772
- }
83773
- const merged = await pullCloudTriage(sharedStore, cloudNow(), {
83774
- stack: [...stack],
83775
- domains: [...domains]
83776
- });
83777
- return { merged };
83778
- },
83779
- async syncPrinciplesPrivate() {
83780
- if (!loadConfig().consent.sync) return null;
83781
- return syncPrinciples(sharedStore, cloudNow(), {
83782
- machine: machineId(),
83783
- daemonVersion: DAEMON_VERSION,
83784
- ts: Date.now()
83785
- });
83786
- },
83787
- async syncSkillsAll() {
83788
- let written = 0;
83789
- let pruned2 = 0;
83790
- let stale;
83791
- for (const r of records) {
83792
- const s = await r.engine.fetchSkills();
83793
- written += s.written;
83794
- pruned2 += s.pruned;
83795
- if (s.staleDaemon) stale = s.staleDaemon;
83796
- }
83797
- const next = pendingFromServerVerdict(
83798
- stale?.latest,
83799
- loadConfig().updateChannel
83800
- );
83801
- if ((next?.latest ?? null) !== (serverPending?.latest ?? null)) {
83802
- serverPending = next;
83803
- for (const rec of records) rec.engine.markContextDirty();
83804
- }
83805
- return { written, pruned: pruned2 };
83806
- },
83807
- async sessionBoundaryFlush() {
83808
- if (!settleRunning) {
83809
- settleRunning = true;
83810
- try {
83811
- for (const r of records) {
83812
- await r.engine.embedSettled();
83813
- }
83814
- } finally {
83815
- settleRunning = false;
83816
- }
83817
- }
83818
- if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
83819
- beginFlush("instances");
83820
- try {
83821
- flushStage("instances");
83822
- const inst = await daemon.syncInstancesPublic();
83823
- flushStage("skills");
83824
- const skills = await daemon.syncSkillsAll();
83825
- return {
83826
- uploaded: inst.uploaded,
83827
- written: skills.written,
83828
- pruned: skills.pruned,
83829
- ...inst.errors ? { errors: inst.errors } : {}
83830
- };
83831
- } finally {
83832
- endFlush();
84153
+ });
84154
+ return res.accepted;
84155
+ });
84156
+ }
83833
84157
  }
84158
+ return { uploaded, ...errors.length > 0 ? { errors } : {} };
83834
84159
  },
83835
- onSessionBoundary(cb) {
83836
- boundaryListeners.push(cb);
83837
- },
83838
- async stop() {
83839
- clearInterval(idleSweep);
83840
- clearInterval(codexWake);
83841
- clearTimeout(codexWakeBoot);
83842
- try {
83843
- const cur = readFileSync30(lockPath, "utf8");
83844
- if (JSON.parse(cur).pid === process.pid) unlinkSync3(lockPath);
83845
- } catch {
83846
- }
83847
- await new Promise(
83848
- (res) => server.close(res)
83849
- );
83850
- updatePoller?.stop();
83851
- healthSentinel.stop();
83852
- for (const r of records) await r.engine.stop();
83853
- if (consolidateWorker) await consolidateWorker.stop();
83854
- try {
83855
- sharedStore.close();
83856
- } catch {
84160
+ async backfillCausalEdges() {
84161
+ if (!loadConfig().consent.sync) return { accepted: 0, rejected: 0, skipped: "consent-off" };
84162
+ const client = cloudNow();
84163
+ let accepted = 0;
84164
+ let rejected = 0;
84165
+ const errors = [];
84166
+ for (const r of records) {
84167
+ const payload = buildCausalEdgeBackfill(r.engine.store, r.engine.profile, DAEMON_VERSION);
84168
+ if (!payload) continue;
84169
+ try {
84170
+ const res = await client.ingest(payload);
84171
+ accepted += res.accepted;
84172
+ rejected += res.rejected;
84173
+ if (res.rejected > 0) errors.push(...res.violations.slice(0, 5));
84174
+ } catch (err2) {
84175
+ errors.push(
84176
+ `[${r.engine.profile.name ?? r.engine.profile.id}] backfill: ${err2 instanceof Error ? err2.message : String(err2)}`
84177
+ );
84178
+ }
83857
84179
  }
83858
- }
83859
- };
83860
- app.post("/api/sync", async (c) => {
83861
- if (boundaryFlushing) return c.json({ skipped: "in-flight", ...flushSnapshot() }, 409);
83862
- beginFlush("principles");
83863
- try {
83864
- const principles = await daemon.syncPrinciplesPublic();
83865
- flushStage("triage");
83866
- const triage = await daemon.syncTriagePublic();
83867
- flushStage("instances");
83868
- const instances = await daemon.syncInstancesPublic();
83869
- flushStage("edge-backfill");
83870
- const edgeBackfill = await daemon.backfillCausalEdges();
83871
- return c.json({ principles, triage, instances, edgeBackfill });
83872
- } catch (err2) {
83873
- return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
83874
- } finally {
83875
- endFlush();
83876
- }
83877
- });
83878
- app.get("/api/sync", (c) => c.json(flushSnapshot()));
83879
- app.post("/api/tick", async (c) => {
83880
- try {
83881
- return c.json({ reports: Object.fromEntries(await daemon.tickAll()) });
83882
- } catch (err2) {
83883
- return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
83884
- }
83885
- });
83886
- return daemon;
83887
- }
83888
-
83889
- // src/cli.ts
83890
- init_src5();
83891
- init_git_remote();
83892
- init_config();
83893
- init_src7();
83894
- init_cloud_auth();
83895
-
83896
- // src/agent-switch.ts
83897
- function parseUseArgs(args2) {
83898
- const positional = [];
83899
- let clear = false;
83900
- for (const arg of args2) {
83901
- if (arg === "--none" || arg === "--clear") {
83902
- clear = true;
83903
- continue;
83904
- }
83905
- if (arg.startsWith("--")) continue;
83906
- positional.push(arg);
83907
- }
83908
- if (clear) return { kind: "clear" };
83909
- const handle2 = positional[0];
83910
- if (!handle2) return { kind: "show" };
83911
- return { kind: "set", handle: handle2 };
83912
- }
83913
- function normalizeHandle(handle2) {
83914
- return handle2.trim().replace(/^@/, "").toLowerCase();
83915
- }
83916
- function findActable(agents, handle2) {
83917
- const want = normalizeHandle(handle2);
83918
- return agents.find((a) => normalizeHandle(a.handle) === want) ?? null;
83919
- }
83920
- async function runUse(action, ctx) {
83921
- switch (action.kind) {
83922
- case "clear":
83923
- return runClear(ctx);
83924
- case "show":
83925
- return runShow(ctx);
83926
- case "set":
83927
- return runSet(action.handle, ctx);
83928
- }
83929
- }
83930
- function runClear(ctx) {
83931
- const active2 = ctx.getActiveAgent();
83932
- ctx.clearActiveAgent();
83933
- if (active2) {
83934
- ctx.log(`no longer acting as ${active2.handle} \u2014 the daemon acts as your default identity`);
83935
- } else {
83936
- ctx.log(`already on the default identity \u2014 nothing to clear`);
83937
- }
83938
- return 0;
83939
- }
83940
- async function runShow(ctx) {
83941
- const active2 = ctx.getActiveAgent();
83942
- ctx.log(active2 ? `active agent: ${active2.handle} (id ${active2.id})` : `active agent: (default identity)`);
83943
- if (!ctx.loggedIn) {
83944
- ctx.log(` log in to list the agents you can act as: errata login`);
83945
- return 0;
83946
- }
83947
- let resp;
83948
- try {
83949
- resp = await ctx.actableAgents();
83950
- } catch (err2) {
83951
- ctx.log(` couldn't list actable agents: ${errText(err2)}`);
83952
- return 1;
83953
- }
83954
- printActable(resp, active2, ctx);
83955
- return 0;
83956
- }
83957
- async function runSet(handle2, ctx) {
83958
- if (!ctx.loggedIn) {
83959
- ctx.log(`not logged in \u2014 run \`errata login\` first, then \`errata use ${handle2}\``);
83960
- return 1;
83961
- }
83962
- let resp;
83963
- try {
83964
- resp = await ctx.actableAgents();
83965
- } catch (err2) {
83966
- ctx.log(`couldn't verify \`${handle2}\`: ${errText(err2)}`);
83967
- return 1;
83968
- }
83969
- const match2 = findActable(resp.agents, handle2);
83970
- if (!match2) {
83971
- ctx.log(`unknown agent: ${handle2}`);
83972
- if (resp.agents.length === 0) {
83973
- ctx.log(` you can't act as any agents yet.`);
83974
- } else {
83975
- ctx.log(` agents you can act as: ${resp.agents.map((a) => a.handle).join(", ")}`);
83976
- }
83977
- return 2;
83978
- }
83979
- ctx.setActiveAgent({ handle: match2.handle, id: match2.id });
83980
- ctx.log(`now acting as ${match2.handle} (id ${match2.id}) \u2014 saved; persists across daemon restarts`);
83981
- return 0;
83982
- }
83983
- function printActable(resp, active2, ctx) {
83984
- if (resp.agents.length === 0) {
83985
- ctx.log(` (no agents you can act as)`);
83986
- return;
83987
- }
83988
- ctx.log(` agents you can act as:`);
83989
- for (const a of resp.agents) {
83990
- const marks = [];
83991
- if (active2 && a.id === active2.id) marks.push("active");
83992
- if (a.id === resp.defaultAgentId) marks.push("default");
83993
- const suffix = marks.length ? ` (${marks.join(", ")})` : "";
83994
- const scope = a.scope ? ` \xB7 ${a.scope}` : "";
83995
- ctx.log(` ${a.handle}${scope}${suffix}`);
83996
- }
83997
- }
83998
- function errText(err2) {
83999
- return err2 instanceof Error ? err2.message : String(err2);
84000
- }
84001
-
84002
- // src/cli.ts
84003
- init_cloud_endpoint_policy();
84004
-
84005
- // src/oauth-login.ts
84006
- init_src7();
84007
- init_cloud_auth();
84008
- import { createServer } from "node:http";
84009
- var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
84010
- var strip = (u) => u.replace(/\/+$/, "");
84011
- function deviceEndpoints(tokenEndpoint) {
84012
- const token = new URL(tokenEndpoint);
84013
- const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
84014
- if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
84015
- const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
84016
- const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
84017
- return {
84018
- code: at(`${prefix}/api/auth/device/code`),
84019
- token: at(`${prefix}/api/auth/device/token`),
84020
- resource: at(`${prefix}/api/gate/session-token`)
84021
- };
84022
- }
84023
- async function loginOAuthDevice(opts) {
84024
- const fetchFn = opts.fetchFn ?? fetch;
84025
- const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
84026
- const device = deviceEndpoints(endpoints.tokenEndpoint);
84027
- const clientId = oauthClientId();
84028
- const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
84029
- const codeResponse = await fetchFn(device.code, {
84030
- method: "POST",
84031
- headers: { "content-type": "application/json", accept: "application/json" },
84032
- body: JSON.stringify({ client_id: clientId, scope })
84033
- });
84034
- if (!codeResponse.ok) {
84035
- throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
84036
- }
84037
- const code = await codeResponse.json();
84038
- if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
84039
- throw new Error("device authorization returned an incomplete response");
84040
- }
84041
- opts.onVerification?.({
84042
- url: code.verification_uri_complete ?? code.verification_uri,
84043
- userCode: code.user_code,
84044
- expiresInSec: code.expires_in
84045
- });
84046
- const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
84047
- let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
84048
- let deviceSession = null;
84049
- let approvedInstallation = null;
84050
- while (Date.now() < deadline) {
84051
- await (opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(intervalMs);
84052
- const poll = await fetchFn(device.token, {
84053
- method: "POST",
84054
- headers: { "content-type": "application/json", accept: "application/json" },
84055
- body: JSON.stringify({
84056
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
84057
- device_code: code.device_code,
84058
- client_id: clientId
84059
- })
84060
- });
84061
- const payload = await poll.json().catch(() => null);
84062
- if (!poll.ok) {
84063
- const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
84064
- if (error48 === "authorization_pending") continue;
84065
- if (error48 === "slow_down") {
84066
- intervalMs += 5e3;
84067
- continue;
84180
+ return { accepted, rejected, ...errors.length > 0 ? { errors } : {} };
84181
+ },
84182
+ async pullTriagePublic() {
84183
+ if (!loadConfig().consent.sync) return { merged: 0, skipped: "consent-off" };
84184
+ const stack = /* @__PURE__ */ new Set();
84185
+ const domains = /* @__PURE__ */ new Set();
84186
+ for (const r of records) {
84187
+ for (const s of r.engine.profile.stack) stack.add(s);
84188
+ for (const d of r.engine.profile.domains) domains.add(d);
84068
84189
  }
84069
- throw new Error(`device authorization failed: ${error48}`);
84070
- }
84071
- deviceSession = typeof payload?.["access_token"] === "string" ? payload["access_token"] : null;
84072
- approvedInstallation = typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null;
84073
- break;
84074
- }
84075
- if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
84076
- if (approvedInstallation !== opts.installationId) {
84077
- throw new Error("approved installation does not match the terminal request");
84078
- }
84079
- const resourceResponse = await fetchFn(device.resource, {
84080
- method: "POST",
84081
- headers: {
84082
- authorization: `Bearer ${deviceSession}`,
84083
- "content-type": "application/json",
84084
- accept: "application/json"
84190
+ const merged = await pullCloudTriage(sharedStore, cloudNow(), {
84191
+ stack: [...stack],
84192
+ domains: [...domains]
84193
+ });
84194
+ return { merged };
84085
84195
  },
84086
- body: JSON.stringify({
84087
- installationId: approvedInstallation,
84088
- clientId,
84089
- scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
84090
- })
84091
- });
84092
- const resource = await resourceResponse.json().catch(() => null);
84093
- if (!resourceResponse.ok || typeof resource?.["access_token"] !== "string") {
84094
- throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
84095
- }
84096
- return {
84097
- installationId: approvedInstallation,
84098
- clientId,
84099
- tokenEndpoint: device.resource,
84100
- tokens: {
84101
- accessToken: resource["access_token"],
84102
- refreshToken: deviceSession,
84103
- ...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
84104
- }
84105
- };
84106
- }
84107
- async function discoverEndpoints(cloudUrl, fetchFn = fetch) {
84108
- let consoleUrl = cloudUrl;
84109
- try {
84110
- const prm = await fetchFn(`${strip(cloudUrl)}/.well-known/oauth-protected-resource`);
84111
- if (prm.ok) {
84112
- const j = await prm.json();
84113
- if (j.authorization_servers?.[0]) consoleUrl = j.authorization_servers[0];
84114
- }
84115
- } catch {
84116
- }
84117
- try {
84118
- const asm = await fetchFn(`${strip(consoleUrl)}/.well-known/oauth-authorization-server`);
84119
- if (asm.ok) {
84120
- const j = await asm.json();
84121
- if (j.authorization_endpoint && j.token_endpoint) {
84196
+ async syncPrinciplesPrivate() {
84197
+ if (!loadConfig().consent.sync) return null;
84198
+ return syncPrinciples(sharedStore, cloudNow(), {
84199
+ machine: machineId(),
84200
+ daemonVersion: DAEMON_VERSION,
84201
+ ts: Date.now()
84202
+ });
84203
+ },
84204
+ async syncSkillsAll() {
84205
+ let written = 0;
84206
+ let pruned2 = 0;
84207
+ let stale;
84208
+ for (const r of records) {
84209
+ const s = await r.engine.fetchSkills();
84210
+ written += s.written;
84211
+ pruned2 += s.pruned;
84212
+ if (s.staleDaemon) stale = s.staleDaemon;
84213
+ }
84214
+ const next = pendingFromServerVerdict(
84215
+ stale?.latest,
84216
+ loadConfig().updateChannel
84217
+ );
84218
+ if ((next?.latest ?? null) !== (serverPending?.latest ?? null)) {
84219
+ serverPending = next;
84220
+ for (const rec of records) rec.engine.markContextDirty();
84221
+ }
84222
+ return { written, pruned: pruned2 };
84223
+ },
84224
+ async sessionBoundaryFlush() {
84225
+ if (!settleRunning) {
84226
+ settleRunning = true;
84227
+ try {
84228
+ for (const r of records) {
84229
+ await r.engine.embedSettled();
84230
+ }
84231
+ } finally {
84232
+ settleRunning = false;
84233
+ }
84234
+ }
84235
+ if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
84236
+ beginFlush("instances");
84237
+ try {
84238
+ flushStage("instances");
84239
+ const inst = await daemon.syncInstancesPublic();
84240
+ flushStage("skills");
84241
+ const skills = await daemon.syncSkillsAll();
84122
84242
  return {
84123
- authorizationEndpoint: j.authorization_endpoint,
84124
- tokenEndpoint: j.token_endpoint,
84125
- ...j.registration_endpoint ? { registrationEndpoint: j.registration_endpoint } : {}
84243
+ uploaded: inst.uploaded,
84244
+ written: skills.written,
84245
+ pruned: skills.pruned,
84246
+ ...inst.errors ? { errors: inst.errors } : {}
84126
84247
  };
84248
+ } finally {
84249
+ endFlush();
84250
+ }
84251
+ },
84252
+ onSessionBoundary(cb) {
84253
+ boundaryListeners.push(cb);
84254
+ },
84255
+ async stop() {
84256
+ clearInterval(idleSweep);
84257
+ clearInterval(codexWake);
84258
+ clearTimeout(codexWakeBoot);
84259
+ try {
84260
+ const cur = readFileSync30(lockPath, "utf8");
84261
+ if (JSON.parse(cur).pid === process.pid) unlinkSync3(lockPath);
84262
+ } catch {
84263
+ }
84264
+ await new Promise(
84265
+ (res) => server.close(res)
84266
+ );
84267
+ updatePoller?.stop();
84268
+ healthSentinel.stop();
84269
+ for (const r of records) await r.engine.stop();
84270
+ if (consolidateWorker) await consolidateWorker.stop();
84271
+ try {
84272
+ sharedStore.close();
84273
+ } catch {
84127
84274
  }
84128
84275
  }
84129
- } catch {
84130
- }
84131
- const base = strip(consoleUrl);
84132
- return {
84133
- authorizationEndpoint: `${base}/api/auth/oauth2/authorize`,
84134
- tokenEndpoint: `${base}/api/auth/oauth2/token`
84135
84276
  };
84136
- }
84137
- function buildAuthorizeUrl(endpoint, p) {
84138
- const u = new URL(endpoint);
84139
- u.searchParams.set("response_type", "code");
84140
- u.searchParams.set("client_id", p.clientId);
84141
- u.searchParams.set("redirect_uri", p.redirectUri);
84142
- u.searchParams.set("scope", p.scope);
84143
- u.searchParams.set("state", p.state);
84144
- u.searchParams.set("code_challenge", p.challenge);
84145
- u.searchParams.set("code_challenge_method", p.method);
84146
- return u.toString();
84147
- }
84148
- async function loginOAuthLoopback(opts) {
84149
- const fetchFn = opts.fetchFn ?? fetch;
84150
- const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
84151
- const pkce = generatePkce();
84152
- const state = randomState();
84153
- const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
84154
- const { code, redirectUri, clientId } = await new Promise(
84155
- (resolve7, reject) => {
84156
- const server = createServer((req, res) => {
84157
- const u = new URL(req.url ?? "/", "http://127.0.0.1");
84158
- if (u.pathname !== "/callback") {
84159
- res.writeHead(404);
84160
- res.end();
84161
- return;
84162
- }
84163
- const addr2 = server.address();
84164
- const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
84165
- const err2 = u.searchParams.get("error");
84166
- const code2 = u.searchParams.get("code");
84167
- const returnedState = u.searchParams.get("state");
84168
- if (err2 || !code2 || returnedState !== state) {
84169
- res.writeHead(400, { "content-type": "text/plain" });
84170
- res.end(`login failed: ${err2 ?? (returnedState !== state ? "state_mismatch" : "no_code")}`);
84171
- server.close();
84172
- reject(new Error(err2 ?? "invalid_callback"));
84173
- return;
84174
- }
84175
- res.writeHead(200, { "content-type": "text/html" });
84176
- res.end("<html><body>inErrata: you're logged in. You can close this tab.</body></html>");
84177
- server.close();
84178
- resolve7({ code: code2, redirectUri: `http://127.0.0.1:${port}/callback`, clientId: activeClientId });
84179
- });
84180
- let activeClientId = oauthClientId();
84181
- server.on("error", reject);
84182
- server.listen(0, "127.0.0.1", () => {
84183
- void (async () => {
84184
- try {
84185
- const addr2 = server.address();
84186
- const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
84187
- const redirectUri2 = `http://127.0.0.1:${port}/callback`;
84188
- if (endpoints.registrationEndpoint) {
84189
- const registered = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
84190
- redirectUri: redirectUri2,
84191
- scope,
84192
- clientName: "errata daemon"
84193
- });
84194
- activeClientId = registered.clientId;
84195
- }
84196
- const url2 = buildAuthorizeUrl(endpoints.authorizationEndpoint, {
84197
- clientId: activeClientId,
84198
- redirectUri: redirectUri2,
84199
- scope,
84200
- state,
84201
- challenge: pkce.challenge,
84202
- method: pkce.method
84203
- });
84204
- if (opts.open) opts.open(url2);
84205
- else console.log(`approve this login in your browser:
84206
- ${url2}`);
84207
- } catch (err2) {
84208
- server.close();
84209
- reject(err2);
84210
- }
84211
- })();
84212
- });
84213
- if (opts.timeoutMs) {
84214
- setTimeout(() => {
84215
- server.close();
84216
- reject(new Error("login timed out \u2014 run `errata login` again"));
84217
- }, opts.timeoutMs).unref?.();
84218
- }
84277
+ app.post("/api/sync", async (c) => {
84278
+ if (boundaryFlushing) return c.json({ skipped: "in-flight", ...flushSnapshot() }, 409);
84279
+ beginFlush("principles");
84280
+ try {
84281
+ const principles = await daemon.syncPrinciplesPublic();
84282
+ flushStage("triage");
84283
+ const triage = await daemon.syncTriagePublic();
84284
+ flushStage("instances");
84285
+ const instances = await daemon.syncInstancesPublic();
84286
+ flushStage("edge-backfill");
84287
+ const edgeBackfill = await daemon.backfillCausalEdges();
84288
+ return c.json({ principles, triage, instances, edgeBackfill });
84289
+ } catch (err2) {
84290
+ return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
84291
+ } finally {
84292
+ endFlush();
84293
+ }
84294
+ });
84295
+ app.get("/api/sync", (c) => c.json(flushSnapshot()));
84296
+ app.post("/api/tick", async (c) => {
84297
+ try {
84298
+ return c.json({ reports: Object.fromEntries(await daemon.tickAll()) });
84299
+ } catch (err2) {
84300
+ return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
84219
84301
  }
84220
- );
84221
- const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
84222
- code,
84223
- codeVerifier: pkce.verifier,
84224
- redirectUri,
84225
- clientId
84226
84302
  });
84227
- return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
84303
+ return daemon;
84228
84304
  }
84229
84305
 
84230
- // src/link-flow.ts
84306
+ // src/cli.ts
84307
+ init_src5();
84308
+ init_git_remote();
84309
+ init_config();
84310
+ init_src7();
84231
84311
  init_cloud_auth();
84232
- import { createHash as createHash15 } from "node:crypto";
84233
- import { execFileSync as execFileSync4 } from "node:child_process";
84234
- import { existsSync as existsSync31, writeFileSync as writeFileSync23 } from "node:fs";
84235
- import { join as join35 } from "node:path";
84236
- function remoteFingerprint(locator) {
84237
- return createHash15("sha256").update(locator).digest("hex");
84312
+
84313
+ // src/agent-switch.ts
84314
+ function parseUseArgs(args2) {
84315
+ const positional = [];
84316
+ let clear = false;
84317
+ for (const arg of args2) {
84318
+ if (arg === "--none" || arg === "--clear") {
84319
+ clear = true;
84320
+ continue;
84321
+ }
84322
+ if (arg.startsWith("--")) continue;
84323
+ positional.push(arg);
84324
+ }
84325
+ if (clear) return { kind: "clear" };
84326
+ const handle2 = positional[0];
84327
+ if (!handle2) return { kind: "show" };
84328
+ return { kind: "set", handle: handle2 };
84238
84329
  }
84239
- function isDecision(value) {
84240
- if (!value || typeof value !== "object") return false;
84241
- const v = value;
84242
- return typeof v["projectId"] === "string" && typeof v["locator"] === "string" && (v["visibility"] === "public" || v["visibility"] === "private") && typeof v["consents"] === "object" && v["consents"] !== null;
84330
+ function normalizeHandle(handle2) {
84331
+ return handle2.trim().replace(/^@/, "").toLowerCase();
84243
84332
  }
84244
- async function linkThroughConsole(opts) {
84245
- const fetchFn = opts.fetchFn ?? fetch;
84246
- const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
84247
- const device = deviceEndpoints(endpoints.tokenEndpoint);
84248
- const clientId = oauthClientId();
84249
- const link = {
84250
- locator: opts.facts.locator,
84251
- remoteFingerprint: remoteFingerprint(opts.facts.locator),
84252
- ...opts.facts.name ? { name: opts.facts.name } : {},
84253
- ...opts.facts.rootCommitSha ? { rootCommitSha: opts.facts.rootCommitSha } : {},
84254
- ...opts.facts.installationId ? { installationId: opts.facts.installationId } : {},
84255
- ...opts.facts.agents && opts.facts.agents.length > 0 ? { agents: opts.facts.agents.slice(0, 12) } : {},
84256
- ...typeof opts.facts.commitCount === "number" ? { commitCount: opts.facts.commitCount } : {}
84257
- };
84258
- const codeResponse = await fetchFn(device.code, {
84259
- method: "POST",
84260
- headers: { "content-type": "application/json", accept: "application/json" },
84261
- body: JSON.stringify({ client_id: clientId, scope: "project:link", link })
84262
- });
84263
- if (!codeResponse.ok) {
84264
- const text = (await codeResponse.text()).slice(0, 200);
84265
- if (codeResponse.status === 404) {
84266
- throw new Error("this console does not offer the link flow yet (device authorization is off)");
84267
- }
84268
- throw new Error(`link request failed: HTTP ${codeResponse.status} ${text}`);
84333
+ function findActable(agents, handle2) {
84334
+ const want = normalizeHandle(handle2);
84335
+ return agents.find((a) => normalizeHandle(a.handle) === want) ?? null;
84336
+ }
84337
+ async function runUse(action, ctx) {
84338
+ switch (action.kind) {
84339
+ case "clear":
84340
+ return runClear(ctx);
84341
+ case "show":
84342
+ return runShow(ctx);
84343
+ case "set":
84344
+ return runSet(action.handle, ctx);
84269
84345
  }
84270
- const code = await codeResponse.json();
84271
- if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
84272
- throw new Error("link request returned an incomplete response");
84346
+ }
84347
+ function runClear(ctx) {
84348
+ const active2 = ctx.getActiveAgent();
84349
+ ctx.clearActiveAgent();
84350
+ if (active2) {
84351
+ ctx.log(`no longer acting as ${active2.handle} \u2014 the daemon acts as your default identity`);
84352
+ } else {
84353
+ ctx.log(`already on the default identity \u2014 nothing to clear`);
84273
84354
  }
84274
- opts.onVerification?.({
84275
- url: code.verification_uri_complete ?? code.verification_uri,
84276
- userCode: code.user_code,
84277
- expiresInSec: code.expires_in
84278
- });
84279
- const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
84280
- let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
84281
- const sleep = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
84282
- while (Date.now() < deadline) {
84283
- await sleep(intervalMs);
84284
- const poll = await fetchFn(device.token, {
84285
- method: "POST",
84286
- headers: { "content-type": "application/json", accept: "application/json" },
84287
- body: JSON.stringify({
84288
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
84289
- device_code: code.device_code,
84290
- client_id: clientId
84291
- })
84292
- });
84293
- const payload = await poll.json().catch(() => null);
84294
- if (!poll.ok) {
84295
- const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
84296
- if (error48 === "authorization_pending") continue;
84297
- if (error48 === "slow_down") {
84298
- intervalMs += 5e3;
84299
- continue;
84300
- }
84301
- if (error48 === "access_denied") return { status: "denied" };
84302
- if (error48 === "expired_token") return { status: "expired" };
84303
- throw new Error(`link approval failed: ${error48}`);
84304
- }
84305
- const decision = payload?.["link"];
84306
- if (!isDecision(decision)) {
84307
- throw new Error("the console approved the device but returned no link decision");
84308
- }
84309
- return {
84310
- status: "approved",
84311
- decision,
84312
- installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
84313
- };
84355
+ return 0;
84356
+ }
84357
+ async function runShow(ctx) {
84358
+ const active2 = ctx.getActiveAgent();
84359
+ ctx.log(active2 ? `active agent: ${active2.handle} (id ${active2.id})` : `active agent: (default identity)`);
84360
+ if (!ctx.loggedIn) {
84361
+ ctx.log(` log in to list the agents you can act as: errata login`);
84362
+ return 0;
84314
84363
  }
84315
- return { status: "expired" };
84364
+ let resp;
84365
+ try {
84366
+ resp = await ctx.actableAgents();
84367
+ } catch (err2) {
84368
+ ctx.log(` couldn't list actable agents: ${errText(err2)}`);
84369
+ return 1;
84370
+ }
84371
+ printActable(resp, active2, ctx);
84372
+ return 0;
84316
84373
  }
84317
- function collectRepoFacts(root) {
84318
- const git3 = (args2) => {
84319
- try {
84320
- return execFileSync4("git", args2, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
84321
- } catch {
84322
- return null;
84374
+ async function runSet(handle2, ctx) {
84375
+ if (!ctx.loggedIn) {
84376
+ ctx.log(`not logged in \u2014 run \`errata login\` first, then \`errata use ${handle2}\``);
84377
+ return 1;
84378
+ }
84379
+ let resp;
84380
+ try {
84381
+ resp = await ctx.actableAgents();
84382
+ } catch (err2) {
84383
+ ctx.log(`couldn't verify \`${handle2}\`: ${errText(err2)}`);
84384
+ return 1;
84385
+ }
84386
+ const match2 = findActable(resp.agents, handle2);
84387
+ if (!match2) {
84388
+ ctx.log(`unknown agent: ${handle2}`);
84389
+ if (resp.agents.length === 0) {
84390
+ ctx.log(` you can't act as any agents yet.`);
84391
+ } else {
84392
+ ctx.log(` agents you can act as: ${resp.agents.map((a) => a.handle).join(", ")}`);
84323
84393
  }
84324
- };
84325
- const roots = git3(["rev-list", "--max-parents=0", "HEAD"]);
84326
- const count = git3(["rev-list", "--count", "HEAD"]);
84327
- const rootCommitSha = roots?.split(/\s+/).filter(Boolean).pop();
84328
- const commitCount = count && /^\d+$/.test(count) ? Number(count) : void 0;
84329
- return {
84330
- ...rootCommitSha && /^[0-9a-f]{40,64}$/i.test(rootCommitSha) ? { rootCommitSha } : {},
84331
- ...typeof commitCount === "number" ? { commitCount } : {}
84332
- };
84394
+ return 2;
84395
+ }
84396
+ ctx.setActiveAgent({ handle: match2.handle, id: match2.id });
84397
+ ctx.log(`now acting as ${match2.handle} (id ${match2.id}) \u2014 saved; persists across daemon restarts`);
84398
+ return 0;
84333
84399
  }
84334
- var IGNORE_STARTER = `# .errataignore \u2014 the hard control over what errata may read or send.
84335
- # Paths, content keywords and tags listed here are dropped from hunks and
84336
- # narrative before anything leaves this machine, and an ignored path yields no
84337
- # symbol anchor at all. Scrubbing is best effort; this list is absolute.
84338
- # A personal list can only add to the organization's list, never weaken it.
84339
-
84340
- # secrets and environment
84341
- .env
84342
- .env.*
84343
- *.pem
84344
- *.key
84345
- *.p12
84346
- secrets/
84347
- credentials*
84348
-
84349
- # fixtures and captured data
84350
- fixtures/
84351
- **/testdata/
84352
- *.dump
84353
- *.sql.gz
84354
- `;
84355
- function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
84356
- const c = decision.consents;
84357
- const nextConfig = {
84358
- ...config2,
84359
- consent: {
84360
- ...config2.consent,
84361
- sync: c.sync !== "off",
84362
- telemetry: c.telemetry,
84363
- contributePackages: c.packages && c.sync !== "off"
84364
- },
84365
- onboardedAt: config2.onboardedAt ?? now()
84366
- };
84367
- const nextProfile = {
84368
- ...profile,
84369
- syncConsent: c.sync === "automatic" ? "automatic" : c.sync === "ask" ? "ask-each-time" : "disabled",
84370
- projectId: decision.projectId,
84371
- projectLocator: decision.locator,
84372
- linkConsents: {
84373
- structure: c.structure,
84374
- mineHistory: c.mineHistory,
84375
- contributeHistory: c.contributeHistory && c.sync !== "off",
84376
- decidedAt: decision.approvedAt
84377
- }
84378
- };
84379
- delete nextProfile.projectLinkDisabled;
84380
- let wroteIgnore = false;
84381
- if (decision.writeIgnoreStarter !== false) {
84382
- const target = join35(root, ".errataignore");
84383
- if (!existsSync31(target)) {
84384
- writeFileSync23(target, IGNORE_STARTER, "utf8");
84385
- wroteIgnore = true;
84386
- }
84400
+ function printActable(resp, active2, ctx) {
84401
+ if (resp.agents.length === 0) {
84402
+ ctx.log(` (no agents you can act as)`);
84403
+ return;
84404
+ }
84405
+ ctx.log(` agents you can act as:`);
84406
+ for (const a of resp.agents) {
84407
+ const marks = [];
84408
+ if (active2 && a.id === active2.id) marks.push("active");
84409
+ if (a.id === resp.defaultAgentId) marks.push("default");
84410
+ const suffix = marks.length ? ` (${marks.join(", ")})` : "";
84411
+ const scope = a.scope ? ` \xB7 ${a.scope}` : "";
84412
+ ctx.log(` ${a.handle}${scope}${suffix}`);
84387
84413
  }
84388
- return { profile: nextProfile, config: nextConfig, wroteIgnore };
84389
84414
  }
84415
+ function errText(err2) {
84416
+ return err2 instanceof Error ? err2.message : String(err2);
84417
+ }
84418
+
84419
+ // src/cli.ts
84420
+ init_cloud_endpoint_policy();
84390
84421
 
84391
84422
  // src/device-login.ts
84392
84423
  init_src7();
@@ -86070,6 +86101,13 @@ async function cmdLink(args2) {
86070
86101
  }
86071
86102
  const consents = historyConsents(applied.profile);
86072
86103
  const reported = await reportHistoryStage(authedCloudClient(applied.config), applied.profile, (line) => console.warn(line));
86104
+ if (reported?.consents) {
86105
+ const synced = applyConsents(applied.profile, applied.config, reported.consents, (/* @__PURE__ */ new Date()).toISOString());
86106
+ if (synced.changed) {
86107
+ saveProfile(ROOT, synced.profile);
86108
+ saveConfig(synced.config);
86109
+ }
86110
+ }
86073
86111
  if (!consents.mineHistory) console.log("history: not mined (consent off)");
86074
86112
  else if (reported?.stopRequested) console.log("history: a stop request is standing in the console; the daemon honours it");
86075
86113
  else console.log(`history: consents recorded${consents.contributeHistory ? "" : " (mine only)"} \xB7 the daemon walks the repo on its next pass`);