@indigoai-us/hq-cli 5.264.0 → 5.266.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.266.0] — 2026-09-28
6
+
7
+ ### Added
8
+
9
+ - `hq daemon run --host desktop` lets the HQ desktop app run the daemon as its
10
+ own child process. The daemon then runs sync even though the app is open, and
11
+ records the app as its host in `state.json`. Older per-service installers
12
+ such as `hq mesh daemon install` treat a hosted daemon as installed and hand
13
+ their service to it.
14
+
15
+ ## [5.265.0] — 2026-09-28
16
+
17
+ ### Added
18
+
19
+ - The `lanes.kr-freshness` flag tracks each manager KPI in a runtime-owned
20
+ ledger and queues one corrective resume for each stalled KPI set. The
21
+ freshness window defaults to six hours and can be set with
22
+ `kr_freshness_hours` in the lanes config.
23
+
24
+ ### Fixed
25
+
26
+ - Reconcile resolves a missing lane company for keep-alive and supersession
27
+ flag reads, and watch applies the same resolution to keep-alive checks.
28
+ Unusable project ownership remains unresolved; reconcile reports
29
+ `keep_alive_company_unresolved` or `keep_alive_flag_off` instead of silently
30
+ skipping those candidates.
31
+
5
32
  ## [5.264.0] — 2026-09-28
6
33
 
7
34
  ### Removed
@@ -7537,6 +7537,22 @@ export declare const COMMAND_CATALOG: readonly [{
7537
7537
  }];
7538
7538
  readonly subcommands: readonly [];
7539
7539
  }];
7540
+ }, {
7541
+ readonly name: "kpis";
7542
+ readonly description: "Show runtime freshness for a manager lane's key results";
7543
+ readonly aliases: readonly [];
7544
+ readonly hidden: false;
7545
+ readonly usage: "[options] <lane>";
7546
+ readonly arguments: readonly [{
7547
+ readonly name: "lane";
7548
+ readonly required: true;
7549
+ readonly variadic: false;
7550
+ }];
7551
+ readonly options: readonly [{
7552
+ readonly flags: "--json";
7553
+ readonly description: "Print the KR freshness report as JSON";
7554
+ }];
7555
+ readonly subcommands: readonly [];
7540
7556
  }, {
7541
7557
  readonly name: "create";
7542
7558
  readonly description: "Register a lane, run admission, and return immediately";
@@ -8962,6 +8978,9 @@ export declare const COMMAND_CATALOG: readonly [{
8962
8978
  }, {
8963
8979
  readonly flags: "--managed";
8964
8980
  readonly description: "Started by the OS unit, which starts the daemon again whenever it exits";
8981
+ }, {
8982
+ readonly flags: "--host <app>";
8983
+ readonly description: "Run as a child of this app (only \"desktop\"), which has handed sync over to the daemon";
8965
8984
  }];
8966
8985
  readonly subcommands: readonly [];
8967
8986
  }, {
@@ -9742,6 +9742,27 @@ export const COMMAND_CATALOG = [
9742
9742
  }
9743
9743
  ]
9744
9744
  },
9745
+ {
9746
+ "name": "kpis",
9747
+ "description": "Show runtime freshness for a manager lane's key results",
9748
+ "aliases": [],
9749
+ "hidden": false,
9750
+ "usage": "[options] <lane>",
9751
+ "arguments": [
9752
+ {
9753
+ "name": "lane",
9754
+ "required": true,
9755
+ "variadic": false
9756
+ }
9757
+ ],
9758
+ "options": [
9759
+ {
9760
+ "flags": "--json",
9761
+ "description": "Print the KR freshness report as JSON"
9762
+ }
9763
+ ],
9764
+ "subcommands": []
9765
+ },
9745
9766
  {
9746
9767
  "name": "create",
9747
9768
  "description": "Register a lane, run admission, and return immediately",
@@ -11590,6 +11611,10 @@ export const COMMAND_CATALOG = [
11590
11611
  {
11591
11612
  "flags": "--managed",
11592
11613
  "description": "Started by the OS unit, which starts the daemon again whenever it exits"
11614
+ },
11615
+ {
11616
+ "flags": "--host <app>",
11617
+ "description": "Run as a child of this app (only \"desktop\"), which has handed sync over to the daemon"
11593
11618
  }
11594
11619
  ],
11595
11620
  "subcommands": []
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type { Command } from "commander";
17
17
  import { type LegacyUnit } from "../lib/daemon/legacy.js";
18
+ import type { HostApp } from "../lib/daemon/supervisor.js";
18
19
  import type { RunCommand } from "../lib/service-manager/launchd.js";
19
20
  export interface DaemonCommandDeps {
20
21
  env: NodeJS.ProcessEnv;
@@ -46,6 +47,8 @@ export interface DaemonCommandDeps {
46
47
  * (HQ_ROOT, the tree around the current folder, the folder the installer or
47
48
  * menubar recorded, the well-known locations). Undefined when none resolves.
48
49
  */
50
+ /** Validate `hq daemon run --host`; the desktop app is the only host. */
51
+ export declare function parseDaemonHost(value: string | undefined): HostApp | undefined;
49
52
  export declare function resolveDaemonHqRoot(flag: string | undefined, where: {
50
53
  env: NodeJS.ProcessEnv;
51
54
  cwd: string;
@@ -36,6 +36,14 @@ class DaemonCommandError extends Error {
36
36
  * (HQ_ROOT, the tree around the current folder, the folder the installer or
37
37
  * menubar recorded, the well-known locations). Undefined when none resolves.
38
38
  */
39
+ /** Validate `hq daemon run --host`; the desktop app is the only host. */
40
+ export function parseDaemonHost(value) {
41
+ if (value === undefined)
42
+ return undefined;
43
+ if (value === "desktop")
44
+ return value;
45
+ throw new Error(`--host must be "desktop" (got "${value}")`);
46
+ }
39
47
  export function resolveDaemonHqRoot(flag, where) {
40
48
  if (flag)
41
49
  return path.resolve(flag);
@@ -574,13 +582,16 @@ export function registerDaemonCommand(program, over = {}) {
574
582
  .option("--foreground", "Also print the supervisor log to the terminal")
575
583
  .option("--daemon-dir <path>", "Use this folder instead of ~/.hq/daemon (how the Windows task passes HQ_DAEMON_DIR)")
576
584
  .option("--managed", "Started by the OS unit, which starts the daemon again whenever it exits")
585
+ .option("--host <app>", "Run as a child of this app (only \"desktop\"), which has handed sync over to the daemon")
577
586
  .action(wrap(async (d, opts) => {
587
+ const host = parseDaemonHost(opts.host);
578
588
  const { runDaemon } = await import("../lib/daemon/run.js");
579
589
  // Services inherit this environment, so they see the same folder.
580
590
  const env = opts.daemonDir ? { ...d.env, HQ_DAEMON_DIR: opts.daemonDir } : d.env;
581
591
  process.exitCode = await runDaemon({
582
592
  foreground: opts.foreground,
583
593
  managed: opts.managed,
594
+ host,
584
595
  home: d.home,
585
596
  env,
586
597
  nodeBinary: d.nodeBinary,
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ export interface LaneKpisCommandDeps {
3
+ hqRoot: () => string;
4
+ now: () => Date;
5
+ wrap: WrapHandler;
6
+ }
7
+ type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
8
+ /** Register the operator report command without depending on the large lanes registrar. */
9
+ export declare function registerLaneKpisCommand(parent: Command, deps: LaneKpisCommandDeps): void;
10
+ export {};
11
+ //# sourceMappingURL=lanes-kpis.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { readKrFreshnessReport } from "../lib/lanes/kr-freshness.js";
2
+ /** Register the operator report command without depending on the large lanes registrar. */
3
+ export function registerLaneKpisCommand(parent, deps) {
4
+ parent
5
+ .command("kpis")
6
+ .description("Show runtime freshness for a manager lane's key results")
7
+ .argument("<lane>")
8
+ .option("--json", "Print the KR freshness report as JSON")
9
+ .action(deps.wrap(async (laneId, options) => {
10
+ const report = readKrFreshnessReport(deps.hqRoot(), laneId, deps.now().getTime());
11
+ const result = {
12
+ ok: true,
13
+ action: "kpis",
14
+ lane_id: laneId,
15
+ ledger_available: report.available,
16
+ ledger_path: report.ledger_path,
17
+ kr_freshness_hours: report.kr_freshness_hours,
18
+ kpis: report.kpis,
19
+ };
20
+ if (options.json) {
21
+ console.log(JSON.stringify(result));
22
+ return;
23
+ }
24
+ if (!report.available) {
25
+ console.log(`No KR freshness ledger exists for ${laneId}. Expected ${report.ledger_path}.`);
26
+ return;
27
+ }
28
+ console.log(`KR freshness for ${laneId} (window ${report.kr_freshness_hours}h):`);
29
+ for (const kpi of report.kpis) {
30
+ console.log(`${kpi.name}: ${JSON.stringify(kpi.value)} — ${kpi.verdict}; measured ${kpi.measured_at}; value changed ${kpi.value_changed_at}`);
31
+ }
32
+ }));
33
+ }
34
+ //# sourceMappingURL=lanes-kpis.js.map
@@ -1,5 +1,6 @@
1
1
  import { accountStatus, coveredLaneIds, loadLanesConfig, MAX_WATCHED_LANES, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, monitorReminderText, runLaneWatch, } from "../lib/lanes/index.js";
2
2
  import { LEAD_LANE_KEEP_ALIVE_FLAG } from "../lib/lanes/keep-alive.js";
3
+ import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
3
4
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
4
5
  import { isLanesError, LanesError } from "../lib/lanes/errors.js";
5
6
  import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
@@ -46,7 +47,7 @@ export function createLaneWatchHandlers(deps) {
46
47
  intervalMs: rawInterval,
47
48
  sleep: ctx.sleep,
48
49
  keepAliveEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
49
- company: lane.company,
50
+ company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
50
51
  env: ctx.env,
51
52
  }),
52
53
  readAccountStatuses: async (lanes) => {
@@ -18,6 +18,7 @@ import { createLaneResumeHandlers } from "./lanes-resume.js";
18
18
  import { createLaneWatchHandlers } from "./lanes-watch.js";
19
19
  export { resolveAnswerChoice } from "./lanes-questions.js";
20
20
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
21
+ import { registerLaneKpisCommand } from "./lanes-kpis.js";
21
22
  import { registerLaneChecksCommand } from "./lanes-checks.js";
22
23
  import { registerLaneReportCommands } from "./lanes-report.js";
23
24
  import { registerLaneTrackerImportCommand } from "./lanes-import.js";
@@ -50,6 +51,7 @@ import { effectiveAccountHome } from "../lib/lanes/account-home.js";
50
51
  import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
51
52
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
52
53
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
54
+ import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
53
55
  import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
54
56
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
55
57
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
@@ -60,6 +62,7 @@ import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
60
62
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
61
63
  import { managerStatusGateEnabledForLane, resolveManagerStatusGateForLane, } from "../lib/lanes/manager-status-gate.js";
62
64
  import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "../lib/lanes/blocker-freshness.js";
65
+ import { resolveKrFreshnessForShow } from "../lib/lanes/kr-freshness.js";
63
66
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
64
67
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
65
68
  import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
@@ -4524,6 +4527,7 @@ async function runShow(ctx, laneId) {
4524
4527
  : storedLane;
4525
4528
  const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
4526
4529
  const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
4530
+ const keepAliveCompany = resolveLaneCompany(hqRoot, storedLane);
4527
4531
  const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
4528
4532
  ? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete }, storedLane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, storedLane))
4529
4533
  : null;
@@ -4533,6 +4537,10 @@ async function runShow(ctx, laneId) {
4533
4537
  env: ctx.env,
4534
4538
  now: ctx.now().getTime(),
4535
4539
  });
4540
+ const krFreshness = await resolveKrFreshnessForShow(hqRoot, storedLane, {
4541
+ env: ctx.env,
4542
+ now: ctx.now().getTime(),
4543
+ });
4536
4544
  printJson({
4537
4545
  ok: true,
4538
4546
  action: "show",
@@ -4562,6 +4570,8 @@ async function runShow(ctx, laneId) {
4562
4570
  keep_alive_state: {
4563
4571
  enabled: keepAliveState.enabled,
4564
4572
  source: keepAliveState.source,
4573
+ company_source: keepAliveCompany.source,
4574
+ company_slug: keepAliveCompany.company?.slug ?? null,
4565
4575
  standing: storedLane.keep_alive_standing === true,
4566
4576
  done_state_verdict: doneStateVerdict,
4567
4577
  opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
@@ -4597,6 +4607,7 @@ async function runShow(ctx, laneId) {
4597
4607
  blockerFreshnessFingerprint(blockerFreshness.stale),
4598
4608
  }
4599
4609
  : null,
4610
+ kr_freshness: krFreshness,
4600
4611
  last_message_at: lane.last_message_at ?? null,
4601
4612
  last_compaction: lane.last_compaction
4602
4613
  ? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
@@ -6918,6 +6929,7 @@ export function registerLanesCommand(parent, deps = {}) {
6918
6929
  env: ctx.env,
6919
6930
  }, wrap);
6920
6931
  registerOutpostPrunerCommand(lanes);
6932
+ registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
6921
6933
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
6922
6934
  noStoryOption.attributeName = () => "noStory";
6923
6935
  lanes
@@ -24,6 +24,8 @@ export declare function daemonRunsService(paths: DaemonPaths, service: ServiceNa
24
24
  export declare function askDaemonToStart(paths: DaemonPaths, unitId: string): void;
25
25
  /** Ask the running daemon to stop a unit and keep it down until it is started again. */
26
26
  export declare function askDaemonToStop(paths: DaemonPaths, unitId: string): void;
27
+ /** A live daemon runs as the desktop app's child, which counts as installed even though no OS unit exists. */
28
+ export declare function hostedDaemonRunning(paths: DaemonPaths, alive?: (pid: number) => boolean): boolean;
27
29
  export interface DaemonUnitHost {
28
30
  platform: NodeJS.Platform;
29
31
  home: string;
@@ -67,6 +67,15 @@ export function askDaemonToStart(paths, unitId) {
67
67
  export function askDaemonToStop(paths, unitId) {
68
68
  writeControlRequest(paths.requestsDir, { action: "stop", unit: unitId });
69
69
  }
70
+ /** A live daemon runs as the desktop app's child, which counts as installed even though no OS unit exists. */
71
+ export function hostedDaemonRunning(paths, alive = (pid) => isDaemonProcess(pid)) {
72
+ try {
73
+ return liveState(paths, alive)?.host === "desktop";
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
70
79
  /** The `hq daemon` OS unit is installed on this machine (for this user, or as the box's system unit). */
71
80
  export function daemonUnitInstalled(host) {
72
81
  switch (host.platform) {
@@ -114,14 +123,14 @@ export function handOffInstall(opts) {
114
123
  /** handOffInstall for this machine and user, for the older install commands. */
115
124
  export function handOffInstallHere(services, env = process.env, opts = {}) {
116
125
  const home = os.homedir();
126
+ const paths = daemonPaths(home, env);
117
127
  const installed = daemonUnitInstalled({
118
128
  platform: process.platform,
119
129
  home,
120
130
  windowsTaskExists: (task) => spawnSync("schtasks.exe", ["/Query", "/TN", task], { encoding: "utf8", windowsHide: true }).status === 0,
121
- });
131
+ }) || hostedDaemonRunning(paths);
122
132
  if (!installed)
123
133
  return null;
124
- const paths = daemonPaths(home, env);
125
134
  const config = readDaemonConfig(paths.configPath);
126
135
  const machineType = detectMachineType(undefined, {
127
136
  platform: process.platform,
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import { type UnitPlan } from "./catalog.js";
9
9
  import { type LegacyUnit } from "./legacy.js";
10
+ import { type MachineType } from "./machine-type.js";
11
+ import { type HostApp } from "./supervisor.js";
10
12
  export declare const LOG_ROTATE_BYTES: number;
11
13
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
12
14
  export declare const RESTART_EXIT_CODE = 75;
@@ -14,6 +16,8 @@ export interface RunDaemonOptions {
14
16
  foreground?: boolean;
15
17
  /** Started by the OS unit, which relaunches the daemon whenever it exits. */
16
18
  managed?: boolean;
19
+ /** The app that runs this daemon as its child (it relaunches the daemon on RESTART_EXIT_CODE when also managed). */
20
+ host?: HostApp;
17
21
  home?: string;
18
22
  env?: NodeJS.ProcessEnv;
19
23
  nodeBinary: string;
@@ -35,6 +39,21 @@ export declare function findUserBinary(name: string, env: NodeJS.ProcessEnv, hom
35
39
  export declare function cliSelfUpdatable(): boolean;
36
40
  /** The HQ desktop app process is running here (it runs its own sync). */
37
41
  export declare function desktopAppRunning(platform: NodeJS.Platform): boolean;
42
+ /** A parent (the OS unit, or the app hosting the daemon) starts the daemon again after it exits with RESTART_EXIT_CODE. */
43
+ export declare function restartsForParent(opts: {
44
+ managed?: boolean;
45
+ host?: HostApp;
46
+ }): boolean;
47
+ /**
48
+ * The desktop app runs sync itself, so the daemon's sync unit waits. When the
49
+ * app hosts the daemon it has handed sync over, and the daemon runs it.
50
+ */
51
+ export declare function desktopAppManagesSync(opts: {
52
+ machineType: MachineType;
53
+ host?: HostApp;
54
+ legacy: readonly LegacyUnit[];
55
+ appRunning: () => boolean;
56
+ }): boolean;
38
57
  /** Copy-and-truncate, so children appending with O_APPEND keep writing to the live file. */
39
58
  export declare function rotateLogs(logsDir: string, maxBytes?: number): number;
40
59
  /**
@@ -133,6 +133,19 @@ export function desktopAppRunning(platform) {
133
133
  const r = spawnSync("pgrep", ["-x", "hq-sync-menubar"], { encoding: "utf8" });
134
134
  return r.status === 0;
135
135
  }
136
+ /** A parent (the OS unit, or the app hosting the daemon) starts the daemon again after it exits with RESTART_EXIT_CODE. */
137
+ export function restartsForParent(opts) {
138
+ return opts.managed === true || opts.host !== undefined;
139
+ }
140
+ /**
141
+ * The desktop app runs sync itself, so the daemon's sync unit waits. When the
142
+ * app hosts the daemon it has handed sync over, and the daemon runs it.
143
+ */
144
+ export function desktopAppManagesSync(opts) {
145
+ if (opts.machineType !== "desktop" || opts.host === "desktop")
146
+ return false;
147
+ return opts.legacy.some((u) => u.service === "sync" && !u.migratable) || opts.appRunning();
148
+ }
136
149
  function jobsReconcileScript(hqRoot) {
137
150
  if (!hqRoot)
138
151
  return null;
@@ -231,7 +244,8 @@ export async function runDaemon(opts) {
231
244
  releaseDaemonLock(paths.lockPath);
232
245
  return 1;
233
246
  }
234
- log("info", `starting hq daemon (pid ${process.pid}) as ${machineType} (${detected.detail})`);
247
+ const hostNote = opts.host ? `, hosted by the ${opts.host} app` : "";
248
+ log("info", `starting hq daemon (pid ${process.pid}) as ${machineType} (${detected.detail})${hostNote}`);
235
249
  if (!resolveSyncRunnerPath())
236
250
  log("warn", "the hq-cloud sync runner was not found in this CLI install; sync is unavailable");
237
251
  const updatable = cliSelfUpdatable();
@@ -270,8 +284,12 @@ export async function runDaemon(opts) {
270
284
  legacySyncEnv: readLegacySyncEnv(platform),
271
285
  signedIn: signedIn(env, home, log),
272
286
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
273
- desktopAppManagesSync: machineType === "desktop" &&
274
- (legacy.some((u) => u.service === "sync" && !u.migratable) || desktopAppRunning(platform)),
287
+ desktopAppManagesSync: desktopAppManagesSync({
288
+ machineType,
289
+ host: opts.host,
290
+ legacy,
291
+ appRunning: () => desktopAppRunning(platform),
292
+ }),
275
293
  bots: listLocalBots(),
276
294
  legacyOwner: legacyOwnerLookup(legacy),
277
295
  cliSelfUpdatable: updatable,
@@ -312,6 +330,7 @@ export async function runDaemon(opts) {
312
330
  let restartDaemon = () => { };
313
331
  const supervisor = createSupervisor({
314
332
  machineType,
333
+ host: opts.host,
315
334
  pid: process.pid,
316
335
  plan,
317
336
  spawn: spawnUnit,
@@ -352,7 +371,7 @@ export async function runDaemon(opts) {
352
371
  });
353
372
  };
354
373
  restartDaemon = () => {
355
- if (!opts.managed) {
374
+ if (!restartsForParent(opts)) {
356
375
  log("warn", "a restart was requested, but no service manager would start this daemon again; restart it yourself");
357
376
  return;
358
377
  }
@@ -50,16 +50,21 @@ export interface UnitState {
50
50
  nextRunAt?: number;
51
51
  lastResult?: "ok" | "failed";
52
52
  }
53
+ /** An app that runs the daemon as its own child process instead of an OS unit. */
54
+ export type HostApp = "desktop";
53
55
  export interface DaemonState {
54
56
  version: 1;
55
57
  pid: number;
56
58
  startedAt: number;
57
59
  updatedAt: number;
58
60
  machineType: MachineType;
61
+ /** Set when an app runs the daemon as its child; absent under the OS unit. */
62
+ host?: HostApp;
59
63
  units: UnitState[];
60
64
  }
61
65
  export interface SupervisorDeps {
62
66
  machineType: MachineType;
67
+ host?: HostApp;
63
68
  pid: number;
64
69
  plan: () => UnitPlan[];
65
70
  spawn: (unit: UnitPlan) => SupervisedChild;
@@ -52,6 +52,7 @@ export function createSupervisor(deps) {
52
52
  startedAt,
53
53
  updatedAt: now(),
54
54
  machineType: deps.machineType,
55
+ ...(deps.host ? { host: deps.host } : {}),
55
56
  units: [...slots.values()].map((s) => ({ ...s.state })),
56
57
  });
57
58
  const emit = () => {
@@ -41,6 +41,8 @@ export declare const DEFAULT_SILENT_LOST_MS: number;
41
41
  export declare const DEFAULT_SESSION_QUESTION_TIMEOUT_SECS: number;
42
42
  /** Open manager blockers are re-verified every four hours by default. */
43
43
  export declare const DEFAULT_BLOCKER_FRESHNESS_HOURS = 4;
44
+ /** Manager key results must be re-measured within six hours by default. */
45
+ export declare const DEFAULT_KR_FRESHNESS_HOURS = 6;
44
46
  export declare const DEFAULT_TRACKER_CHECKS: TrackerChecksConfig;
45
47
  export declare const DEFAULT_LIVENESS_WINDOWS: LivenessWindows;
46
48
  export declare const DEFAULT_PROVIDER_MAX_RUNNING: Record<LaneProvider, number>;
@@ -45,6 +45,8 @@ export const DEFAULT_SILENT_LOST_MS = 12 * 60 * 60 * 1000;
45
45
  export const DEFAULT_SESSION_QUESTION_TIMEOUT_SECS = 10 * 60;
46
46
  /** Open manager blockers are re-verified every four hours by default. */
47
47
  export const DEFAULT_BLOCKER_FRESHNESS_HOURS = 4;
48
+ /** Manager key results must be re-measured within six hours by default. */
49
+ export const DEFAULT_KR_FRESHNESS_HOURS = 6;
48
50
  export const DEFAULT_TRACKER_CHECKS = {
49
51
  owner_question_surface_ms: 10 * 60 * 1000,
50
52
  open_question_age_ms: 2 * 60 * 60 * 1000,
@@ -425,6 +427,7 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
425
427
  tracker: { checks: { ...DEFAULT_TRACKER_CHECKS } },
426
428
  session_question_timeout_secs: DEFAULT_SESSION_QUESTION_TIMEOUT_SECS,
427
429
  blocker_freshness_hours: DEFAULT_BLOCKER_FRESHNESS_HOURS,
430
+ kr_freshness_hours: DEFAULT_KR_FRESHNESS_HOURS,
428
431
  };
429
432
  }
430
433
  export function parseLanesConfig(raw, deps = defaultDeps()) {
@@ -468,6 +471,7 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
468
471
  tracker: { checks: parseTrackerChecks(raw.tracker) },
469
472
  session_question_timeout_secs: positiveInt(raw.session_question_timeout_secs, "session_question_timeout_secs", DEFAULT_SESSION_QUESTION_TIMEOUT_SECS),
470
473
  blocker_freshness_hours: positiveNumber(raw.blocker_freshness_hours, "blocker_freshness_hours", DEFAULT_BLOCKER_FRESHNESS_HOURS),
474
+ kr_freshness_hours: positiveNumber(raw.kr_freshness_hours, "kr_freshness_hours", DEFAULT_KR_FRESHNESS_HOURS),
471
475
  };
472
476
  }
473
477
  export function lanesConfigPath(hqRoot) {
@@ -40,7 +40,7 @@ export declare function retiredLaneRegistryPath(hqRoot: string): string;
40
40
  export declare function loadRetiredLaneIds(hqRoot: string): ReadonlySet<string>;
41
41
  /** IDs may be stored bare or with an owner-host suffix after an underscore. */
42
42
  export declare function isRetiredLaneId(laneId: string, retiredLaneIds: ReadonlySet<string>): boolean;
43
- export declare function keepAliveSupersededBy(lane: LaneRecord, lanes: readonly LaneRecord[], lineageAware?: boolean): LaneRecord | undefined;
44
- export declare function keepAliveExclusion(lane: LaneRecord, lanes: readonly LaneRecord[], retiredLaneIds: ReadonlySet<string>, lineageAware?: boolean): "retired" | "superseded" | undefined;
43
+ export declare function keepAliveSupersededBy(lane: LaneRecord, lanes: readonly LaneRecord[], lineageAware?: boolean, companyForLane?: (lane: LaneRecord) => LaneRecord["company"]): LaneRecord | undefined;
44
+ export declare function keepAliveExclusion(lane: LaneRecord, lanes: readonly LaneRecord[], retiredLaneIds: ReadonlySet<string>, lineageAware?: boolean, companyForLane?: (lane: LaneRecord) => LaneRecord["company"]): "retired" | "superseded" | undefined;
45
45
  export declare function leadLaneKeepAliveBackoffMs(attempt: number): number;
46
46
  //# sourceMappingURL=keep-alive.d.ts.map
@@ -199,10 +199,12 @@ export function isRetiredLaneId(laneId, retiredLaneIds) {
199
199
  }
200
200
  return false;
201
201
  }
202
- export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED) {
202
+ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED, companyForLane) {
203
203
  const createdAt = Date.parse(lane.created_at);
204
204
  if (!Number.isFinite(createdAt))
205
205
  return undefined;
206
+ const companyOf = companyForLane ?? ((candidate) => candidate.company);
207
+ const laneCompany = companyOf(lane);
206
208
  const lanesById = new Map(lanes.map((candidate) => [candidate.lane_id, candidate]));
207
209
  const targetAncestorIds = new Set();
208
210
  if (lineageAware) {
@@ -218,9 +220,10 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
218
220
  return lanes.find((candidate) => {
219
221
  if (candidate.lane_id === lane.lane_id || Date.parse(candidate.created_at) <= createdAt)
220
222
  return false;
221
- const sameCompany = lane.company === null
222
- ? candidate.company === null
223
- : candidate.company?.uid === lane.company.uid;
223
+ const candidateCompany = companyOf(candidate);
224
+ const sameCompany = laneCompany === null
225
+ ? candidateCompany === null
226
+ : candidateCompany?.uid === laneCompany.uid;
224
227
  if (!sameCompany)
225
228
  return false;
226
229
  if (lineageAware && targetAncestorIds.has(candidate.lane_id))
@@ -249,10 +252,10 @@ export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESS
249
252
  return sameProfile || projectLead;
250
253
  });
251
254
  }
252
- export function keepAliveExclusion(lane, lanes, retiredLaneIds, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED) {
255
+ export function keepAliveExclusion(lane, lanes, retiredLaneIds, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED, companyForLane) {
253
256
  if (isRetiredLaneId(lane.lane_id, retiredLaneIds))
254
257
  return "retired";
255
- return keepAliveSupersededBy(lane, lanes, lineageAware) === undefined ? undefined : "superseded";
258
+ return keepAliveSupersededBy(lane, lanes, lineageAware, companyForLane) === undefined ? undefined : "superseded";
256
259
  }
257
260
  export function leadLaneKeepAliveBackoffMs(attempt) {
258
261
  if (!Number.isSafeInteger(attempt) || attempt < 1) {
@@ -0,0 +1,76 @@
1
+ import { type ManagerStatusTarget } from "./manager-status-gate.js";
2
+ import { readLanesRegistryFlag } from "./registry-flag.js";
3
+ import type { LaneRecord } from "./types.js";
4
+ export declare const KR_FRESHNESS_FLAG = "lanes.kr-freshness";
5
+ export declare const KR_FRESHNESS_DEFAULT_HOURS = 6;
6
+ export declare const KR_FRESHNESS_LEDGER_REL: string;
7
+ export declare const KR_FRESHNESS_LEDGER_SCHEMA = "kr-freshness-ledger.v1";
8
+ export declare const KR_FRESHNESS_HISTORY_LIMIT = 50;
9
+ export type KrFreshnessVerdict = "moving" | "flat" | "stalled";
10
+ export interface KrFreshnessKpi {
11
+ name: string;
12
+ value: number | string;
13
+ entry_measured_at: string | null;
14
+ first_seen_at: string;
15
+ measured_at: string;
16
+ value_changed_at: string;
17
+ removed_at?: string;
18
+ }
19
+ export interface KrFreshnessLedger {
20
+ schema_version: typeof KR_FRESHNESS_LEDGER_SCHEMA;
21
+ lane_id: string;
22
+ updated_at: string;
23
+ kpis: Record<string, KrFreshnessKpi>;
24
+ }
25
+ export interface KrFreshnessView {
26
+ name: string;
27
+ value: number | string;
28
+ verdict: KrFreshnessVerdict;
29
+ measured_at: string;
30
+ value_changed_at: string;
31
+ }
32
+ export interface KrFreshnessResolution {
33
+ flagEnabled: boolean;
34
+ enabled: boolean;
35
+ target?: ManagerStatusTarget;
36
+ statusPath?: string;
37
+ windowHours: number;
38
+ kpis: KrFreshnessView[];
39
+ stalledKeys: string[];
40
+ error?: string;
41
+ }
42
+ export interface KrFreshnessResolutionOptions {
43
+ env?: NodeJS.ProcessEnv;
44
+ now?: number;
45
+ readFlag?: typeof readLanesRegistryFlag;
46
+ onError?: (error: unknown) => void;
47
+ }
48
+ export interface KrFreshnessStatusKpi {
49
+ key: string;
50
+ fullName: string;
51
+ name: string;
52
+ value: number | string;
53
+ entryMeasuredAt: string | null;
54
+ }
55
+ export declare function readKrFreshnessStatus(statusPath: string): KrFreshnessStatusKpi[];
56
+ export declare function krFreshnessLedgerPath(root: string, laneId: string): string;
57
+ export declare function readKrFreshnessLedger(root: string, laneId: string): KrFreshnessLedger | null;
58
+ export declare function writeKrFreshnessLedger(root: string, ledger: KrFreshnessLedger): void;
59
+ export declare function updateKrFreshnessLedger(laneId: string, prior: KrFreshnessLedger | null, statusKpis: readonly KrFreshnessStatusKpi[], now: number): KrFreshnessLedger;
60
+ export declare function krFreshnessVerdict(kpi: Pick<KrFreshnessKpi, "measured_at" | "value_changed_at">, now: number, windowHours: number): KrFreshnessVerdict;
61
+ export declare function krFreshnessFingerprint(keys: readonly string[]): string;
62
+ /** Read the flag before manager mapping, status, configuration or ledger files. */
63
+ export declare function resolveKrFreshnessForLane(root: string, lane: LaneRecord, options?: KrFreshnessResolutionOptions): Promise<KrFreshnessResolution>;
64
+ /** Read a previously written ledger for `lanes show`; this never reads status. */
65
+ export declare function resolveKrFreshnessForShow(root: string, lane: LaneRecord, options?: KrFreshnessResolutionOptions): Promise<{
66
+ enabled: true;
67
+ kpis: KrFreshnessView[];
68
+ } | null>;
69
+ /** Operator report path: the ledger remains readable while runtime enforcement is off. */
70
+ export declare function readKrFreshnessReport(root: string, laneId: string, now: number): {
71
+ ledger_path: string;
72
+ available: boolean;
73
+ kr_freshness_hours: number;
74
+ kpis: KrFreshnessView[];
75
+ };
76
+ //# sourceMappingURL=kr-freshness.d.ts.map