@indigoai-us/hq-cli 5.345.199 → 5.345.200

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 (41) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +6 -4
  3. package/dist/command-catalog.generated.d.ts +44 -0
  4. package/dist/command-catalog.generated.js +52 -0
  5. package/dist/commands/bot-runtime.d.ts +35 -0
  6. package/dist/commands/bot-runtime.js +115 -0
  7. package/dist/commands/bot.d.ts +6 -0
  8. package/dist/commands/bot.js +39 -10
  9. package/dist/commands/creators.js +2 -1
  10. package/dist/commands/library-remove.js +2 -1
  11. package/dist/commands/library.js +5 -4
  12. package/dist/commands/mesh.d.ts +0 -7
  13. package/dist/commands/mesh.js +5 -1
  14. package/dist/commands/sync-held.js +2 -2
  15. package/dist/lib/bot/fleet-download.d.ts +27 -0
  16. package/dist/lib/bot/fleet-download.js +162 -0
  17. package/dist/lib/bot/fleet-local.d.ts +51 -2
  18. package/dist/lib/bot/fleet-local.js +177 -74
  19. package/dist/lib/bot/fleet-python.d.ts +2 -0
  20. package/dist/lib/bot/fleet-python.js +29 -0
  21. package/dist/lib/lanes/mesh-events.js +3 -1
  22. package/dist/lib/mesh/api.js +2 -1
  23. package/dist/lib/mesh/live/daemon/run.d.ts +0 -10
  24. package/dist/lib/mesh/live/daemon/run.js +3 -0
  25. package/dist/lib/mesh/live/emit.d.ts +2 -11
  26. package/dist/lib/mesh/live/emit.js +20 -2
  27. package/dist/lib/mesh/live/flush.d.ts +2 -0
  28. package/dist/lib/mesh/live/flush.js +34 -3
  29. package/dist/lib/mesh/live/reporting-privacy.d.ts +7 -0
  30. package/dist/lib/mesh/live/reporting-privacy.js +47 -0
  31. package/dist/lib/work-context/company.d.ts +2 -3
  32. package/dist/lib/work-context/company.js +4 -22
  33. package/dist/secrets-env-action.js +5 -2
  34. package/dist/utils/cognito-callback-port.d.ts +2 -0
  35. package/dist/utils/cognito-callback-port.js +10 -5
  36. package/dist/utils/cognito-config.js +6 -1
  37. package/dist/utils/cognito-session.js +6 -1
  38. package/dist/utils/interactive-caller.d.ts +8 -0
  39. package/dist/utils/interactive-caller.js +10 -0
  40. package/dist/utils/login-browser.js +2 -1
  41. package/package.json +7 -7
@@ -6,6 +6,7 @@ import * as path from "node:path";
6
6
  import * as zlib from "node:zlib";
7
7
  import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
8
8
  import { withBotLock } from "./lock.js";
9
+ import { downloadFleetFile, fileSha256 } from "./fleet-download.js";
9
10
  import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
10
11
  import semver from "semver";
11
12
  import { CLI_VERSION } from "../../cli-version.js";
@@ -79,14 +80,14 @@ export function sharedRuntimeLayout(root, id, platform) {
79
80
  }
80
81
  export const HQ_FLEET_LOCAL_RELEASE = {
81
82
  repository: "indigoai-us/hq-fleet-local",
82
- tag: "v0.4.24",
83
- version: "0.4.24",
84
- artifact: "hq-fleet-local-0.4.24.zip",
85
- artifactSha256: "daa74b7aaf7f26d8c069f33aa94a453369fb2d7ae8c122e7fbe48a5b38e73f31",
86
- runtimeArtifact: "hq-fleet-runtime-d257fdd1171f.tar",
87
- runtimeArtifactSha256: "8262c5f28773d35c04f7f9ce6f61ed19a88e601847f9d12d5ab11fb5d8e87fb0",
88
- manifestSha256: "302d2206957b815a846db583a49e7862948fd8a8a36aaff8e0e8a6e0aa7a1b9c",
89
- signatureSha256: "efee90b00c6eb1d0f53a76e93a756435975c9da6c70c9c0b505aafcce79a586a",
83
+ tag: "v0.4.25",
84
+ version: "0.4.25",
85
+ artifact: "hq-fleet-local-0.4.25.zip",
86
+ artifactSha256: "4d32e04f992600ab0f1656de3536358dfdba77bfccdc23e1b01ba7a0de4ea38d",
87
+ runtimeArtifact: "hq-fleet-runtime-d257fdd1171f.tar.gz",
88
+ runtimeArtifactSha256: "4d26d250eb100efff280bcd254e06e3a2993875c1fff4eec1733eaf99e52cd70",
89
+ manifestSha256: "d7e3a3309bf60e6366b192c9131d44166edf63cb5a60ca7923fe30145fc890f0",
90
+ signatureSha256: "1b018c39e69aefa26a0b1f95ed5d97388fd4747851643c8a53cae3069c670c0a",
90
91
  runtimeCommit: "d257fdd1171f1edd5e381b3bad7baacab0cb00d3",
91
92
  };
92
93
  /** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
@@ -470,7 +471,8 @@ export function parseFleetLocalReleasePin(value) {
470
471
  return null;
471
472
  if (!COMMIT_RE.test(pin.runtimeCommit))
472
473
  return null;
473
- if (pin.runtimeArtifact !== `hq-fleet-runtime-${pin.runtimeCommit.slice(0, 12)}.tar`)
474
+ const runtimeStem = `hq-fleet-runtime-${pin.runtimeCommit.slice(0, 12)}`;
475
+ if (pin.runtimeArtifact !== `${runtimeStem}.tar` && pin.runtimeArtifact !== `${runtimeStem}.tar.gz`)
474
476
  return null;
475
477
  if (![pin.artifactSha256, pin.runtimeArtifactSha256, pin.manifestSha256, pin.signatureSha256].every((hash) => SHA256_RE.test(hash))) {
476
478
  return null;
@@ -637,38 +639,67 @@ function trustedReleaseLink(url) {
637
639
  return false;
638
640
  }
639
641
  }
640
- async function downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime) {
641
- const get = async (name, timeoutMs) => {
642
+ export const FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS = 30_000;
643
+ /** One machine-wide lock: concurrent creates and adopts share one download instead of repeating it. */
644
+ export const FLEET_LOCAL_DISTRIBUTION_LOCK_NAME = "hq-fleet-local-distribution";
645
+ export const FLEET_LOCAL_DISTRIBUTION_LOCK_WAIT_MS = 30 * 60_000;
646
+ export const FLEET_LOCAL_DISTRIBUTION_LOCK_STALE_MS = 60 * 60_000;
647
+ const RELEASE_FILE_NAMES = (release) => [
648
+ release.artifact,
649
+ release.runtimeArtifact,
650
+ "release-manifest.json",
651
+ "release-manifest.sig",
652
+ ];
653
+ function downloadsDir(root, release) {
654
+ return path.join(root, "distributions", "hq-fleet-local", ".downloads", release.version);
655
+ }
656
+ function discardDownloads(target) {
657
+ fs.rmSync(target, { recursive: true, force: true });
658
+ try {
659
+ fs.rmdirSync(path.dirname(target));
660
+ }
661
+ catch {
662
+ return;
663
+ }
664
+ }
665
+ async function downloadReleaseFiles(release, fetcher, resolve, cachedRuntime, target, onProgress, stallMs) {
666
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
667
+ const downloaded = new Map();
668
+ for (const name of RELEASE_FILE_NAMES(release)) {
669
+ if (name === release.runtimeArtifact && cachedRuntime) {
670
+ downloaded.set(name, cachedRuntime);
671
+ continue;
672
+ }
673
+ const { url, headers } = resolve(name);
674
+ const file = await downloadFleetFile(url, path.join(target, name), {
675
+ fetch: fetcher,
676
+ ...(headers ? { headers } : {}),
677
+ ...(stallMs !== undefined ? { stallMs } : {}),
678
+ onProgress: (progress) => onProgress?.({ phase: "download", file: name, received: progress.received, total: progress.total }),
679
+ });
680
+ downloaded.set(name, file);
681
+ }
682
+ return {
683
+ archive: fs.readFileSync(downloaded.get(release.artifact)),
684
+ runtimeArchive: fs.readFileSync(downloaded.get(release.runtimeArtifact)),
685
+ manifest: fs.readFileSync(downloaded.get("release-manifest.json")),
686
+ signature: fs.readFileSync(downloaded.get("release-manifest.sig")),
687
+ };
688
+ }
689
+ async function downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime, target, onProgress, stallMs) {
690
+ for (const name of RELEASE_FILE_NAMES(release)) {
691
+ if (name === release.runtimeArtifact && cachedRuntime)
692
+ continue;
642
693
  const url = links.get(name);
643
694
  if (!url)
644
695
  throw expectedError(`HQ Fleet Local ${release.tag} is missing release asset ${name}.`);
645
696
  if (!trustedReleaseLink(url)) {
646
697
  throw expectedError(`HQ Fleet Local ${release.tag} returned an untrusted download link for ${name}.`);
647
698
  }
648
- const response = await fetcher(url, { signal: AbortSignal.timeout(timeoutMs) });
649
- if (!response.ok) {
650
- throw expectedError(`Could not download ${name}: HQ storage returned HTTP ${response.status}.`);
651
- }
652
- return Buffer.from(await response.arrayBuffer());
653
- };
654
- const [archive, runtimeArchive, manifest, signature] = await Promise.all([
655
- get(release.artifact, FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS),
656
- cachedRuntime ?? get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
657
- get("release-manifest.json", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
658
- get("release-manifest.sig", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
659
- ]);
660
- return { archive, runtimeArchive, manifest, signature };
661
- }
662
- async function responseBytes(response, label) {
663
- if (!response.ok) {
664
- throw expectedError(`Could not download ${label}: GitHub returned HTTP ${response.status}.`);
665
699
  }
666
- return Buffer.from(await response.arrayBuffer());
700
+ return downloadReleaseFiles(release, fetcher, (name) => ({ url: links.get(name) }), cachedRuntime, target, onProgress, stallMs);
667
701
  }
668
- export const FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS = 30_000;
669
- export const FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
670
- export const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS = 20 * 60_000;
671
- async function downloadFleetLocalRelease(release, fetcher, token, cachedRuntime) {
702
+ async function downloadFleetLocalRelease(release, fetcher, token, cachedRuntime, target, onProgress, stallMs) {
672
703
  const headers = {
673
704
  Accept: "application/vnd.github+json",
674
705
  Authorization: `Bearer ${token}`,
@@ -676,7 +707,7 @@ async function downloadFleetLocalRelease(release, fetcher, token, cachedRuntime)
676
707
  "X-GitHub-Api-Version": "2022-11-28",
677
708
  };
678
709
  const releaseUrl = `https://api.github.com/repos/${release.repository}/releases/tags/${release.tag}`;
679
- const response = await fetcher(releaseUrl, { headers, signal: AbortSignal.timeout(30_000) });
710
+ const response = await fetcher(releaseUrl, { headers, signal: AbortSignal.timeout(FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS) });
680
711
  if (!response.ok) {
681
712
  throw expectedError(`Could not read HQ Fleet Local ${release.tag}: GitHub returned HTTP ${response.status}.`);
682
713
  }
@@ -684,25 +715,17 @@ async function downloadFleetLocalRelease(release, fetcher, token, cachedRuntime)
684
715
  const assets = new Map((body.assets ?? [])
685
716
  .filter((asset) => !!asset.name && !!asset.url)
686
717
  .map((asset) => [asset.name, asset.url]));
687
- const get = async (name, timeoutMs) => {
718
+ for (const name of RELEASE_FILE_NAMES(release)) {
719
+ if (name === release.runtimeArtifact && cachedRuntime)
720
+ continue;
688
721
  const url = assets.get(name);
689
722
  if (!url)
690
723
  throw expectedError(`HQ Fleet Local ${release.tag} is missing release asset ${name}.`);
691
724
  if (new URL(url).origin !== "https://api.github.com") {
692
725
  throw expectedError(`HQ Fleet Local ${release.tag} returned an untrusted asset URL for ${name}.`);
693
726
  }
694
- return responseBytes(await fetcher(url, {
695
- headers: { ...headers, Accept: "application/octet-stream" },
696
- signal: AbortSignal.timeout(timeoutMs),
697
- }), name);
698
- };
699
- const [archive, runtimeArchive, manifest, signature] = await Promise.all([
700
- get(release.artifact, FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS),
701
- cachedRuntime ?? get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
702
- get("release-manifest.json", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
703
- get("release-manifest.sig", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
704
- ]);
705
- return { archive, runtimeArchive, manifest, signature };
727
+ }
728
+ return downloadReleaseFiles(release, fetcher, (name) => ({ url: assets.get(name), headers: { ...headers, Accept: "application/octet-stream" } }), cachedRuntime, target, onProgress, stallMs);
706
729
  }
707
730
  function installedFiles(dir, release) {
708
731
  const archive = path.join(dir, release.artifact);
@@ -749,49 +772,83 @@ function installFiles(target, files, release, publicKey) {
749
772
  fs.rmSync(staging, { recursive: true, force: true });
750
773
  }
751
774
  }
775
+ /** A runtime archive already on this machine whose bytes match the pin: any installed release, or a finished download. */
752
776
  function cachedRuntimeArchive(root, release) {
753
777
  const distributions = path.join(root, "distributions", "hq-fleet-local");
754
778
  if (!fs.existsSync(distributions))
755
779
  return null;
756
- for (const entry of fs.readdirSync(distributions, { withFileTypes: true })) {
757
- if (!entry.isDirectory() || entry.name.startsWith("."))
758
- continue;
759
- const directory = path.join(distributions, entry.name);
780
+ const candidates = [];
781
+ const visit = (directory) => {
760
782
  for (const file of fs.readdirSync(directory, { withFileTypes: true })) {
761
- if (!file.isFile() || !file.name.startsWith("hq-fleet-runtime-") || !file.name.endsWith(".tar"))
783
+ const full = path.join(directory, file.name);
784
+ if (file.isDirectory()) {
785
+ if (file.name === ".downloads" || !file.name.startsWith("."))
786
+ visit(full);
787
+ continue;
788
+ }
789
+ if (!file.isFile() || !file.name.startsWith("hq-fleet-runtime-"))
762
790
  continue;
763
- const bytes = fs.readFileSync(path.join(directory, file.name));
764
- if (digest(bytes) === release.runtimeArtifactSha256)
765
- return bytes;
791
+ if (!file.name.endsWith(".tar") && !file.name.endsWith(".tar.gz"))
792
+ continue;
793
+ candidates.push(full);
766
794
  }
795
+ };
796
+ visit(distributions);
797
+ for (const candidate of candidates) {
798
+ if (fileSha256(candidate) === release.runtimeArtifactSha256)
799
+ return candidate;
767
800
  }
768
801
  return null;
769
802
  }
803
+ /** Whether the pinned release is installed on this machine (no download needed). */
804
+ export function fleetLocalDistributionInstalled(release = HQ_FLEET_LOCAL_RELEASE, root = fleetHome()) {
805
+ return installedFiles(path.join(root, "distributions", "hq-fleet-local", release.version), release) !== null;
806
+ }
770
807
  export async function installPinnedFleetLocalDistribution(deps = {}) {
771
808
  const release = deps.release ?? HQ_FLEET_LOCAL_RELEASE;
772
809
  const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEYS;
773
810
  const root = deps.fleetHome ?? fleetHome();
774
811
  const dir = path.join(root, "distributions", "hq-fleet-local", release.version);
775
- const current = installedFiles(dir, release);
812
+ const onProgress = deps.onProgress;
776
813
  const warnCompatibility = (manifest) => {
777
814
  const warning = fleetLocalCliCompatibilityWarning(manifest, release);
778
815
  if (warning)
779
816
  process.stderr.write(`Warning: ${warning}\n`);
780
817
  };
818
+ const installation = () => ({
819
+ release,
820
+ dir,
821
+ archive: path.join(dir, release.artifact),
822
+ runtimeArchive: path.join(dir, release.runtimeArtifact),
823
+ manifest: path.join(dir, "release-manifest.json"),
824
+ signature: path.join(dir, "release-manifest.sig"),
825
+ engine: engineMetadata(release),
826
+ });
827
+ const current = installedFiles(dir, release);
781
828
  if (current) {
782
829
  verifyFleetLocalRelease(current, release, publicKey);
783
830
  warnCompatibility(current.manifest);
784
- }
785
- else {
831
+ onProgress?.({ phase: "ready", step: "download", cached: true });
832
+ return installation();
833
+ }
834
+ return withBotLock(FLEET_LOCAL_DISTRIBUTION_LOCK_NAME, async () => {
835
+ const again = installedFiles(dir, release);
836
+ if (again) {
837
+ verifyFleetLocalRelease(again, release, publicKey);
838
+ warnCompatibility(again.manifest);
839
+ onProgress?.({ phase: "ready", step: "download", cached: true });
840
+ return installation();
841
+ }
786
842
  const fetcher = deps.fetch ?? fetch;
787
843
  const lookup = deps.hqReleaseLinks
788
844
  ? { links: await deps.hqReleaseLinks(release), reason: null }
789
845
  : await (deps.hqReleaseLookup ?? lookupHqFleetReleaseLinks)(release);
790
846
  const links = lookup.links;
791
847
  const cachedRuntime = cachedRuntimeArchive(root, release);
848
+ const target = downloadsDir(root, release);
792
849
  let files;
793
850
  if (links) {
794
- files = await downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime);
851
+ files = await downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime, target, onProgress, deps.downloadStallMs);
795
852
  }
796
853
  else {
797
854
  let githubToken;
@@ -799,24 +856,29 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
799
856
  githubToken = (deps.token ?? resolveFleetReleaseToken)();
800
857
  }
801
858
  catch (error) {
802
- // A customer has no GitHub token; say why HQ had no download instead of asking them to sign in again.
803
859
  throw noReleaseSourceError(release, lookup.reason) ?? error;
804
860
  }
805
- files = await downloadFleetLocalRelease(release, fetcher, githubToken, cachedRuntime);
861
+ files = await downloadFleetLocalRelease(release, fetcher, githubToken, cachedRuntime, target, onProgress, deps.downloadStallMs);
862
+ }
863
+ onProgress?.({ phase: "verify" });
864
+ try {
865
+ verifyFleetLocalRelease(files, release, publicKey);
866
+ }
867
+ catch (error) {
868
+ discardDownloads(target);
869
+ throw error;
806
870
  }
807
- verifyFleetLocalRelease(files, release, publicKey);
808
871
  warnCompatibility(files.manifest);
809
872
  installFiles(dir, files, release, publicKey);
810
- }
811
- return {
812
- release,
813
- dir,
814
- archive: path.join(dir, release.artifact),
815
- runtimeArchive: path.join(dir, release.runtimeArtifact),
816
- manifest: path.join(dir, "release-manifest.json"),
817
- signature: path.join(dir, "release-manifest.sig"),
818
- engine: engineMetadata(release),
819
- };
873
+ discardDownloads(target);
874
+ onProgress?.({ phase: "ready", step: "download", cached: false });
875
+ return installation();
876
+ }, {
877
+ root: path.join(root, "distributions"),
878
+ command: "HQ Fleet Runtime download",
879
+ waitMs: FLEET_LOCAL_DISTRIBUTION_LOCK_WAIT_MS,
880
+ staleMs: FLEET_LOCAL_DISTRIBUTION_LOCK_STALE_MS,
881
+ });
820
882
  }
821
883
  function profileName(agentUid) {
822
884
  return agentUid.toLowerCase();
@@ -1633,7 +1695,7 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
1633
1695
  if (!opts.keepSource)
1634
1696
  fs.rmSync(machineCredentials, { force: true });
1635
1697
  }
1636
- async function installSharedFleetRuntime(installation, root, python, platform, run, spawn) {
1698
+ async function installSharedFleetRuntime(installation, root, python, platform, run, spawn, onProgress) {
1637
1699
  const identity = digest(Buffer.from(JSON.stringify({
1638
1700
  version: installation.release.version,
1639
1701
  distribution: installation.release.artifactSha256,
@@ -1672,9 +1734,11 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1672
1734
  if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
1673
1735
  throw expectedError("The shared HQ Fleet Runtime is incomplete. Reinstall it before creating another bot.");
1674
1736
  }
1737
+ onProgress?.({ phase: "ready", step: "runtime", cached: true });
1675
1738
  return layout;
1676
1739
  }
1677
1740
  fs.rmSync(shared, { recursive: true, force: true });
1741
+ onProgress?.({ phase: "extract" });
1678
1742
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
1679
1743
  fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
1680
1744
  const uvEnv = fleetUvEnv(python.pythonDir);
@@ -1689,6 +1753,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1689
1753
  installation.runtimeArchive,
1690
1754
  runtimeSource,
1691
1755
  ], { stdio: "ignore" }, "runtime extraction");
1756
+ onProgress?.({ phase: "environment" });
1692
1757
  checkedRun(run, python.uv, [
1693
1758
  "venv",
1694
1759
  "--no-config",
@@ -1697,6 +1762,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1697
1762
  runtimeDir,
1698
1763
  ], { stdio: "ignore", env: uvEnv }, "environment creation");
1699
1764
  const runtimePython = venvExecutable(runtimeDir, "python", platform);
1765
+ onProgress?.({ phase: "packages" });
1700
1766
  checkedRun(run, python.uv, [
1701
1767
  "pip",
1702
1768
  "install",
@@ -1711,6 +1777,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
1711
1777
  throw expectedError("HQ Fleet Local installed without its runtime executables.");
1712
1778
  }
1713
1779
  fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, runtimeId: identity, engine: installation.engine })}\n`, { mode: 0o600 });
1780
+ onProgress?.({ phase: "ready", step: "runtime", cached: false });
1714
1781
  return layout;
1715
1782
  }, { root: path.join(root, "runtimes"), command: "HQ Fleet Runtime installation", waitMs: 20 * 60_000, staleMs: 2 * 60 * 60_000 });
1716
1783
  }
@@ -1725,6 +1792,42 @@ function removeEmbeddedRuntime(profileDir, platform = process.platform) {
1725
1792
  if (hasEmbeddedRuntime(profileDir, platform))
1726
1793
  fs.rmSync(path.join(profileDir, "runtime"), { recursive: true, force: true });
1727
1794
  }
1795
+ /**
1796
+ * Everything a bot needs that is shared by every bot on this machine: the
1797
+ * pinned distribution and runtime archive, the private Python, and the built
1798
+ * runtime environment. Safe to run any number of times; a complete install
1799
+ * returns at once.
1800
+ */
1801
+ export async function ensureFleetRuntimeInstalled(installation, deps = {}) {
1802
+ const run = deps.run ?? execFileSync;
1803
+ const platform = deps.platform ?? process.platform;
1804
+ const root = deps.fleetHome ?? fleetHome();
1805
+ deps.onProgress?.({ phase: "python" });
1806
+ const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
1807
+ const spawn = deps.spawn ?? (deps.run ? spawnFromRun(deps.run) : spawnSync);
1808
+ const shared = await installSharedFleetRuntime(installation, root, python, platform, run, spawn, deps.onProgress);
1809
+ return { release: installation.release, distributionDir: installation.dir, runtimeDir: shared.runtimeDir, runtimeId: shared.id };
1810
+ }
1811
+ /** Whether the shared runtime for this release is already built on this machine. */
1812
+ export function fleetRuntimeReady(installation, deps = {}) {
1813
+ const root = deps.fleetHome ?? fleetHome();
1814
+ const platform = deps.platform ?? process.platform;
1815
+ const python = deps.python;
1816
+ if (!python)
1817
+ return false;
1818
+ const identity = digest(Buffer.from(JSON.stringify({
1819
+ version: installation.release.version,
1820
+ distribution: installation.release.artifactSha256,
1821
+ runtime: installation.release.runtimeArtifactSha256,
1822
+ python: python.python,
1823
+ platform,
1824
+ extras: ["mcp", "anthropic"],
1825
+ })));
1826
+ const layout = sharedRuntimeLayout(root, identity, platform);
1827
+ if (!fs.existsSync(path.join(layout.root, "ready.json")))
1828
+ return false;
1829
+ return ["python", "hq-fleet", "hq-fleet-local"].every((name) => fs.existsSync(venvExecutable(layout.runtimeDir, name, platform)));
1830
+ }
1728
1831
  export async function bootstrapFleetLocalBot(input, deps = {}) {
1729
1832
  const run = deps.run ?? execFileSync;
1730
1833
  const platform = deps.platform ?? process.platform;
@@ -1733,7 +1836,7 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
1733
1836
  const profileDir = adoptLegacyProfileDirectory(input.agentUid, root);
1734
1837
  fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
1735
1838
  const spawn = deps.spawn ?? (deps.run ? spawnFromRun(deps.run) : spawnSync);
1736
- const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run, spawn);
1839
+ const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run, spawn, deps.onProgress);
1737
1840
  writeNativeProfileMarker(profileDir, shared, input.installation.engine);
1738
1841
  const { systemPrompt, machineCredentials, ...profileInput } = input;
1739
1842
  if (systemPrompt !== undefined)
@@ -43,6 +43,8 @@ export declare function fleetUvEnv(pythonDir: string, base?: NodeJS.ProcessEnv):
43
43
  /** The operating system's own tar when it is where it always is, so a `tar` on PATH or in the current folder is never run. */
44
44
  export declare function tarExecutable(platform: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
45
45
  export declare function fleetPythonToolPath(deps?: FleetPythonDeps): string;
46
+ /** The installer and private Python already on this machine, or null; never downloads anything. */
47
+ export declare function installedFleetPython(deps?: FleetPythonDeps): FleetPython | null;
46
48
  /**
47
49
  * The pinned installer and private Python, installed once per machine under
48
50
  * the fleet folder and reused by every bot. Concurrent creates wait for each
@@ -222,6 +222,35 @@ async function ensureFleetPythonLocked(root, deps) {
222
222
  }
223
223
  return { uv, python, pythonDir };
224
224
  }
225
+ /** The installer and private Python already on this machine, or null; never downloads anything. */
226
+ export function installedFleetPython(deps = {}) {
227
+ const root = deps.fleetHome ?? fleetHome();
228
+ const platform = deps.platform ?? process.platform;
229
+ const run = deps.run ?? execFileSync;
230
+ const version = deps.pythonVersion ?? HQ_FLEET_PYTHON_VERSION;
231
+ const uv = fleetPythonToolPath(deps);
232
+ const pythonDir = path.join(root, "python");
233
+ if (!installedTool(uv) || !fs.existsSync(pythonDir))
234
+ return null;
235
+ try {
236
+ const output = run(uv, ["python", "find", version, "--managed-python", "--no-config"], {
237
+ stdio: ["ignore", "pipe", "ignore"],
238
+ env: fleetUvEnv(pythonDir, deps.env ?? process.env),
239
+ encoding: "utf8",
240
+ windowsHide: true,
241
+ timeout: FLEET_PYTHON_FIND_TIMEOUT_MS,
242
+ killSignal: "SIGKILL",
243
+ });
244
+ const found = (Buffer.isBuffer(output) ? output.toString("utf8") : output).trim();
245
+ const python = path.resolve(found);
246
+ if (!found || !fs.existsSync(python) || !isInside(pythonDir, python, platform))
247
+ return null;
248
+ return { uv, python, pythonDir };
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
225
254
  /**
226
255
  * The pinned installer and private Python, installed once per machine under
227
256
  * the fleet folder and reused by every bot. Concurrent creates wait for each
@@ -45,6 +45,7 @@
45
45
  import { appendThreadEvent, clamp, ensureLaneMeshThread, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
46
46
  import { LanesError } from "./errors.js";
47
47
  import { terminalEnvelopeStoryStatus } from "./stories.js";
48
+ import { gatedInteractiveLogin } from "../../utils/interactive-caller.js";
48
49
  export const LANE_MESH_EVENTS = [
49
50
  "lane_started",
50
51
  "lane_progress",
@@ -309,7 +310,8 @@ async function repairProjectView(input) {
309
310
  }
310
311
  export function createLaneMesh(opts = {}) {
311
312
  const interactive = opts.interactive ?? true;
312
- const token = opts.token ?? (() => requireToken({ interactive }));
313
+ // Gated per call: the same mesh can serve a terminal and a detached watcher.
314
+ const token = opts.token ?? (() => requireToken({ interactive: gatedInteractiveLogin(interactive) }));
313
315
  const repair = opts.repairProjectView ?? repairProjectView;
314
316
  return {
315
317
  async ensureProjectThread(company, projectId) {
@@ -9,6 +9,7 @@ import { ensureCognitoToken, isMachineIdentity } from "../../utils/cognito-sessi
9
9
  import { peekIdToken } from "../../utils/id-token.js";
10
10
  import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
11
11
  import { meshCacheRoot, removeSessionFallbackFiles, writeMeshCacheFile, } from "./cache.js";
12
+ import { gatedInteractiveLogin } from "../../utils/interactive-caller.js";
12
13
  const PRINCIPAL_UID = /^(prs|agt)_/;
13
14
  export const STORY_STATUSES = new Set([
14
15
  "queued",
@@ -183,7 +184,7 @@ export async function requireToken(opts = {}) {
183
184
  // hard policy indigo-agent-caller-resolution-id-token, agent claims ride
184
185
  // the ID token only; the server is correct to require it. Humans keep the
185
186
  // person access token (unchanged).
186
- const interactive = opts.interactive ?? true;
187
+ const interactive = gatedInteractiveLogin(opts.interactive ?? true);
187
188
  if (isMachineIdentity()) {
188
189
  return ensureCognitoToken({ tokenSource: "machine", interactive });
189
190
  }
@@ -1,13 +1,3 @@
1
- /**
2
- * hq mesh daemon run — single-instance resident process.
3
- *
4
- * - pid lock at ~/.hq/work-mesh/daemon/pid
5
- * - fs.watch spool + 2s debounce; flush at least every 10s (US-008 path)
6
- * - replay context outbox
7
- * - board.md refresh for bound sessions ≤ every 60s
8
- * - MQTT presence (contract-3)
9
- * - SIGTERM/SIGINT: flush once more, MQTT DISCONNECT (server derives offline), exit
10
- */
11
1
  import { type DeliverFn } from "../../../work-context/outbox.js";
12
2
  import { type SessionEventsPoster } from "../session-events-client.js";
13
3
  import { type FlushSummary } from "../flush.js";
@@ -1,3 +1,4 @@
1
+ import { reportingRuntimeAllowed } from "../reporting-privacy.js";
1
2
  /**
2
3
  * hq mesh daemon run — single-instance resident process.
3
4
  *
@@ -380,6 +381,8 @@ export async function runMeshDaemon(deps = {}) {
380
381
  baseUrl: resolveVaultApiBase(env),
381
382
  });
382
383
  return flushSessionEvents({
384
+ env,
385
+ reportingAllowed: () => reportingRuntimeAllowed(machineIdentity, env),
383
386
  workMeshRoot: meshRoot,
384
387
  workContextRoot: ctxRoot,
385
388
  poster,
@@ -1,14 +1,3 @@
1
- /**
2
- * Direct-emit orchestration (owner decision 2026-09-08). One invocation:
3
- * 1. drains the local retry file (prior failures),
4
- * 2. appends the new event(s),
5
- * 3. POSTs /v1/mesh/events in batches with a short bounded retry,
6
- * 4. keeps only network/5xx/429-failed or unaccounted events for next time,
7
- * 5. records lastPostAt + per-event status counts for `hq mesh doctor`.
8
- *
9
- * accepted / unassigned / rejected are all terminal (rejected is dropped with a
10
- * count — never re-posted). No long-lived process, spool, or held queue.
11
- */
12
1
  import { type EmitPoster, type EmitEventStatus } from "./emit-client.js";
13
2
  import { type RandomFn, type SleepFn } from "./backoff.js";
14
3
  export interface EmitStateFile {
@@ -23,6 +12,8 @@ export interface EmitStateFile {
23
12
  export declare function emitStatePath(workMeshRoot: string): string;
24
13
  export declare function readEmitState(workMeshRoot: string): EmitStateFile | null;
25
14
  export interface EmitEventsDeps {
15
+ env?: NodeJS.ProcessEnv;
16
+ reportingAllowed?: () => Promise<boolean>;
26
17
  workMeshRoot: string;
27
18
  poster: EmitPoster;
28
19
  /** New events to emit this invocation (may be empty — just drain retry). */
@@ -1,3 +1,4 @@
1
+ import { reportingLocallyDisabled, mayEmitConversation } from "./reporting-privacy.js";
1
2
  /**
2
3
  * Direct-emit orchestration (owner decision 2026-09-08). One invocation:
3
4
  * 1. drains the local retry file (prior failures),
@@ -90,10 +91,27 @@ export async function emitEvents(deps) {
90
91
  statuses: { accepted: 0, unassigned: 0, rejected: 0 },
91
92
  rejections: [],
92
93
  };
94
+ const env = deps.env ?? process.env;
95
+ if (reportingLocallyDisabled(env) || (deps.reportingAllowed && !(await deps.reportingAllowed()))) {
96
+ const written = writeEmitRetry(deps.workMeshRoot, combined, retryMax);
97
+ return { ...summary, attempted: 0, retained: written.written,
98
+ retryDepth: written.written, droppedOverflow: written.dropped };
99
+ }
100
+ const allowed = combined.filter(event => {
101
+ if (mayEmitConversation(event, env))
102
+ return true;
103
+ summary.rejected += 1;
104
+ summary.statuses.rejected += 1;
105
+ const eventId = eventIdOf(event);
106
+ if (eventId)
107
+ summary.rejections.push({ eventId, code: "PERSONAL_OR_UNATTRIBUTED" });
108
+ return false;
109
+ });
110
+ summary.attempted = allowed.length;
93
111
  const keep = [];
94
112
  let anyPosted = false;
95
- for (let i = 0; i < combined.length; i += MESH_EVENTS_BATCH_MAX) {
96
- const chunk = combined.slice(i, i + MESH_EVENTS_BATCH_MAX);
113
+ for (let i = 0; i < allowed.length; i += MESH_EVENTS_BATCH_MAX) {
114
+ const chunk = allowed.slice(i, i + MESH_EVENTS_BATCH_MAX);
97
115
  summary.batches += 1;
98
116
  let result = await deps.poster(chunk);
99
117
  for (let attempt = 1; attempt < maxAttempts && !result.ok && result.retryable; attempt += 1) {
@@ -24,6 +24,8 @@ export declare const HELD_MAX_LINES = 20000;
24
24
  export declare const PROJECT_REBIND_INTERVAL_MS = 60000;
25
25
  export type HeldReason = "STATE_ABSENT" | "NEEDS_COMPANY" | "COMPANY_CONFLICT" | "NEEDS_PROJECT" | "HELD_EXPIRED" | "HELD_OVERFLOW";
26
26
  export interface FlushDeps {
27
+ /** Production callers re-read the shared person preference before each flush. */
28
+ reportingAllowed?: () => Promise<boolean>;
27
29
  workMeshRoot: string;
28
30
  workContextRoot: string;
29
31
  poster: SessionEventsPoster;