@indigoai-us/hq-cli 5.345.178 → 5.345.180

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 (117) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/command-catalog.generated.d.ts +197 -1
  3. package/dist/command-catalog.generated.js +241 -1
  4. package/dist/commands/bot-jobs.d.ts +11 -9
  5. package/dist/commands/bot-jobs.js +56 -21
  6. package/dist/commands/bot.d.ts +37 -1
  7. package/dist/commands/bot.js +289 -13
  8. package/dist/commands/channels.d.ts +18 -1
  9. package/dist/commands/channels.js +185 -1
  10. package/dist/commands/daemon.d.ts +2 -0
  11. package/dist/commands/daemon.js +15 -4
  12. package/dist/commands/integrations-core.d.ts +2 -0
  13. package/dist/commands/integrations-core.js +1 -0
  14. package/dist/commands/lanes.d.ts +1 -1
  15. package/dist/commands/lanes.js +6 -2
  16. package/dist/commands/mesh.js +19 -0
  17. package/dist/commands/sources.d.ts +89 -0
  18. package/dist/commands/sources.js +363 -1
  19. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  20. package/dist/hq-cloud-sync-runner/package.json +1 -1
  21. package/dist/lib/bot/config.d.ts +8 -0
  22. package/dist/lib/bot/fleet-local.d.ts +1 -1
  23. package/dist/lib/bot/fleet-local.js +82 -30
  24. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
  25. package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
  26. package/dist/lib/bot/local-jobs.d.ts +40 -4
  27. package/dist/lib/bot/local-jobs.js +72 -5
  28. package/dist/lib/bot/run.d.ts +15 -0
  29. package/dist/lib/bot/run.js +99 -1
  30. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  31. package/dist/lib/bot/runtime-auth.js +251 -0
  32. package/dist/lib/daemon/catalog.d.ts +1 -0
  33. package/dist/lib/daemon/catalog.js +1 -1
  34. package/dist/lib/daemon/handoff.d.ts +4 -0
  35. package/dist/lib/daemon/handoff.js +6 -2
  36. package/dist/lib/daemon/install.d.ts +70 -2
  37. package/dist/lib/daemon/install.js +185 -27
  38. package/dist/lib/daemon/legacy.d.ts +9 -3
  39. package/dist/lib/daemon/legacy.js +11 -4
  40. package/dist/lib/daemon/migrate.js +1 -1
  41. package/dist/lib/daemon/run.d.ts +5 -2
  42. package/dist/lib/daemon/run.js +74 -22
  43. package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
  44. package/dist/lib/doctor/checks/anywhere.js +35 -6
  45. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  46. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  47. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
  48. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
  49. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
  50. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
  51. package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
  52. package/dist/lib/hot-channels/adapters/attio.js +374 -0
  53. package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
  54. package/dist/lib/hot-channels/adapters/gmail.js +413 -0
  55. package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
  56. package/dist/lib/hot-channels/adapters/index.js +39 -0
  57. package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
  58. package/dist/lib/hot-channels/adapters/linear.js +321 -0
  59. package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
  60. package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
  61. package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
  62. package/dist/lib/hot-channels/adapters/meetings.js +525 -0
  63. package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
  64. package/dist/lib/hot-channels/adapters/slack.js +277 -0
  65. package/dist/lib/hot-channels/alert.d.ts +78 -0
  66. package/dist/lib/hot-channels/alert.js +179 -0
  67. package/dist/lib/hot-channels/audience.d.ts +15 -0
  68. package/dist/lib/hot-channels/audience.js +25 -0
  69. package/dist/lib/hot-channels/contract.d.ts +146 -0
  70. package/dist/lib/hot-channels/contract.js +20 -0
  71. package/dist/lib/hot-channels/cursors.d.ts +41 -0
  72. package/dist/lib/hot-channels/cursors.js +84 -0
  73. package/dist/lib/hot-channels/index.d.ts +10 -0
  74. package/dist/lib/hot-channels/index.js +10 -0
  75. package/dist/lib/hot-channels/landing.d.ts +83 -0
  76. package/dist/lib/hot-channels/landing.js +175 -0
  77. package/dist/lib/hot-channels/ledger.d.ts +101 -0
  78. package/dist/lib/hot-channels/ledger.js +203 -0
  79. package/dist/lib/hot-channels/loop.d.ts +74 -0
  80. package/dist/lib/hot-channels/loop.js +221 -0
  81. package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
  82. package/dist/lib/hot-channels/registry.d.ts +28 -0
  83. package/dist/lib/hot-channels/registry.js +121 -0
  84. package/dist/lib/hot-channels/run.d.ts +137 -0
  85. package/dist/lib/hot-channels/run.js +324 -0
  86. package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
  87. package/dist/lib/hot-channels/signals-pass.js +581 -0
  88. package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
  89. package/dist/lib/hot-channels/sync-lock.js +224 -0
  90. package/dist/lib/install/hook-target.d.ts +6 -2
  91. package/dist/lib/install/hook-target.js +9 -4
  92. package/dist/lib/lanes/accounts.d.ts +1 -17
  93. package/dist/lib/lanes/accounts.js +5 -45
  94. package/dist/lib/lanes/loop.d.ts +10 -0
  95. package/dist/lib/lanes/loop.js +18 -1
  96. package/dist/lib/lanes/provider-home.d.ts +24 -0
  97. package/dist/lib/lanes/provider-home.js +52 -0
  98. package/dist/lib/lanes/spawn.d.ts +12 -0
  99. package/dist/lib/lanes/spawn.js +27 -0
  100. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  101. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  102. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  103. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  104. package/dist/lib/mesh/live/daemon/run.js +31 -0
  105. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  106. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  107. package/dist/lib/mesh/live/paths.d.ts +2 -0
  108. package/dist/lib/mesh/live/paths.js +2 -0
  109. package/dist/lib/sync-core/flag.d.ts +1 -1
  110. package/dist/lib/sync-core/flag.js +1 -1
  111. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  112. package/dist/lib/sync-core/reconcile.js +9 -7
  113. package/dist/lib/sync-core/release.d.ts +3 -3
  114. package/dist/lib/sync-core/release.js +4 -4
  115. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  116. package/dist/lib/sync-core/rollout-state.js +1 -0
  117. package/package.json +8 -8
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.104","bundleSha256":"40c72e049db21ee4551fa737676d8022cd090149ede9e3c9e53a9fae3f28dadc","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.105","bundleSha256":"f57aa12671cdfcf08ce39f11ce6885d6e52d5478f2ae77a75d38d79162194ce5","formatVersion":1}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * bot.json — non-secret per-bot configuration (local-bots US-003/US-005).
3
3
  */
4
+ import type { BotRuntimeAuthMap } from "./runtime-auth.js";
4
5
  export declare const BOT_RUNTIMES: readonly ["claude", "codex", "grok"];
5
6
  export type BotRuntimeId = (typeof BOT_RUNTIMES)[number];
6
7
  export declare const HQ_FLEET_RUNTIME_ENGINE_ID: "hq-fleet-runtime";
@@ -167,6 +168,13 @@ export interface BotConfig {
167
168
  * setup bots, which only need files and shell commands.
168
169
  */
169
170
  lean?: boolean;
171
+ /**
172
+ * Which model login each runtime uses (see runtime-auth.ts): unset = the
173
+ * owner's default sign-in; { mode: "own" } = the bot's own login under
174
+ * ~/.hq/bots/<name>/auth/<runtime>; { mode: "account", account } = a
175
+ * capacity account home. Names only, never a credential.
176
+ */
177
+ runtimeAuth?: BotRuntimeAuthMap;
170
178
  }
171
179
  export declare function readBotConfig(dir: string): BotConfig | null;
172
180
  export declare function writeBotConfig(dir: string, config: BotConfig): void;
@@ -133,7 +133,7 @@ export interface FleetLocalRemovalResult {
133
133
  credentialsRemoved: number;
134
134
  }
135
135
  export declare const HQ_FLEET_LOCAL_RELEASE: FleetLocalReleasePin;
136
- export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAOOL7NRKQgjMYI5cWxLY+HGppXf2AtwpQbCg5v+nBksM=\n-----END PUBLIC KEY-----\n";
136
+ export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEARXr2I8RNcw2c0Ep77XkYGwylresXBFn8dm9pxN2Zq/U=\n-----END PUBLIC KEY-----\n";
137
137
  export declare function fleetHome(home?: string, env?: NodeJS.ProcessEnv): string;
138
138
  export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string): void;
139
139
  export declare function resolveFleetReleaseToken(env?: NodeJS.ProcessEnv): string;
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { HQ_FLEET_RUNTIME_ENGINE_ID, } from "./config.js";
7
+ import { withBotLock } from "./lock.js";
7
8
  import { ensureFleetPython, fleetUvEnv } from "./fleet-python.js";
8
9
  import { ensureCognitoToken } from "../../utils/cognito-session.js";
9
10
  import { vaultApiFetch } from "../../utils/vault-api.js";
@@ -35,18 +36,18 @@ export function fleetProviderNeedsSignIn(provider) {
35
36
  }
36
37
  export const HQ_FLEET_LOCAL_RELEASE = {
37
38
  repository: "indigoai-us/hq-fleet-local",
38
- tag: "v0.4.12",
39
- version: "0.4.12",
40
- artifact: "hq-fleet-local-0.4.12.zip",
41
- artifactSha256: "2a07e79b1740e8ef68b19078b73b10cdf9377065a2fd1f53287b7e353099c577",
39
+ tag: "v0.4.14",
40
+ version: "0.4.14",
41
+ artifact: "hq-fleet-local-0.4.14.zip",
42
+ artifactSha256: "9ff6ad4f7235988468e1913008c5650f5d064210078d6d20c407b17120310f47",
42
43
  runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
43
44
  runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
44
- manifestSha256: "cdac590bdcebea6629de0c164c42c7baf30c025a299ff5829380989dd22a9ff1",
45
- signatureSha256: "f07911dd3187393cd325fc8f88ddaa6a7568825aee6d8748a992cea822d5d9b1",
45
+ manifestSha256: "88e71a733a5f57089764cbf9947fb88025c2263c5f3d62a2c6f5ab3635538871",
46
+ signatureSha256: "b892b54d6fcb2aaca86d049d7662fe5549a250d81877d6c529288e79ae1565d9",
46
47
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
47
48
  };
48
49
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
49
- MCowBQYDK2VwAyEAOOL7NRKQgjMYI5cWxLY+HGppXf2AtwpQbCg5v+nBksM=
50
+ MCowBQYDK2VwAyEARXr2I8RNcw2c0Ep77XkYGwylresXBFn8dm9pxN2Zq/U=
50
51
  -----END PUBLIC KEY-----
51
52
  `;
52
53
  function digest(value) {
@@ -162,7 +163,7 @@ function trustedReleaseLink(url) {
162
163
  return false;
163
164
  }
164
165
  }
165
- async function downloadFleetLocalReleaseFromHq(release, fetcher, links) {
166
+ async function downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime) {
166
167
  const get = async (name, timeoutMs) => {
167
168
  const url = links.get(name);
168
169
  if (!url)
@@ -178,7 +179,7 @@ async function downloadFleetLocalReleaseFromHq(release, fetcher, links) {
178
179
  };
179
180
  const [archive, runtimeArchive, manifest, signature] = await Promise.all([
180
181
  get(release.artifact, FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS),
181
- get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
182
+ cachedRuntime ?? get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
182
183
  get("release-manifest.json", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
183
184
  get("release-manifest.sig", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
184
185
  ]);
@@ -193,7 +194,7 @@ async function responseBytes(response, label) {
193
194
  export const FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS = 30_000;
194
195
  export const FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
195
196
  export const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS = 20 * 60_000;
196
- async function downloadFleetLocalRelease(release, fetcher, token) {
197
+ async function downloadFleetLocalRelease(release, fetcher, token, cachedRuntime) {
197
198
  const headers = {
198
199
  Accept: "application/vnd.github+json",
199
200
  Authorization: `Bearer ${token}`,
@@ -223,7 +224,7 @@ async function downloadFleetLocalRelease(release, fetcher, token) {
223
224
  };
224
225
  const [archive, runtimeArchive, manifest, signature] = await Promise.all([
225
226
  get(release.artifact, FLEET_LOCAL_ARCHIVE_DOWNLOAD_TIMEOUT_MS),
226
- get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
227
+ cachedRuntime ?? get(release.runtimeArtifact, FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS),
227
228
  get("release-manifest.json", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
228
229
  get("release-manifest.sig", FLEET_LOCAL_METADATA_DOWNLOAD_TIMEOUT_MS),
229
230
  ]);
@@ -274,6 +275,24 @@ function installFiles(target, files, release, publicKey) {
274
275
  fs.rmSync(staging, { recursive: true, force: true });
275
276
  }
276
277
  }
278
+ function cachedRuntimeArchive(root, release) {
279
+ const distributions = path.join(root, "distributions", "hq-fleet-local");
280
+ if (!fs.existsSync(distributions))
281
+ return null;
282
+ for (const entry of fs.readdirSync(distributions, { withFileTypes: true })) {
283
+ if (!entry.isDirectory() || entry.name.startsWith("."))
284
+ continue;
285
+ const directory = path.join(distributions, entry.name);
286
+ for (const file of fs.readdirSync(directory, { withFileTypes: true })) {
287
+ if (!file.isFile() || !file.name.startsWith("hq-fleet-runtime-") || !file.name.endsWith(".tar"))
288
+ continue;
289
+ const bytes = fs.readFileSync(path.join(directory, file.name));
290
+ if (digest(bytes) === release.runtimeArtifactSha256)
291
+ return bytes;
292
+ }
293
+ }
294
+ return null;
295
+ }
277
296
  export async function installPinnedFleetLocalDistribution(deps = {}) {
278
297
  const release = deps.release ?? HQ_FLEET_LOCAL_RELEASE;
279
298
  const publicKey = deps.publicKey ?? HQ_FLEET_LOCAL_PUBLIC_KEY;
@@ -286,9 +305,10 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
286
305
  else {
287
306
  const fetcher = deps.fetch ?? fetch;
288
307
  const links = await (deps.hqReleaseLinks ?? fetchHqFleetReleaseLinks)(release);
308
+ const cachedRuntime = cachedRuntimeArchive(root, release);
289
309
  const files = links
290
- ? await downloadFleetLocalReleaseFromHq(release, fetcher, links)
291
- : await downloadFleetLocalRelease(release, fetcher, (deps.token ?? resolveFleetReleaseToken)());
310
+ ? await downloadFleetLocalReleaseFromHq(release, fetcher, links, cachedRuntime)
311
+ : await downloadFleetLocalRelease(release, fetcher, (deps.token ?? resolveFleetReleaseToken)(), cachedRuntime);
292
312
  verifyFleetLocalRelease(files, release, publicKey);
293
313
  installFiles(dir, files, release, publicKey);
294
314
  }
@@ -744,21 +764,30 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
744
764
  ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
745
765
  fs.rmSync(machineCredentials, { force: true });
746
766
  }
747
- export async function bootstrapFleetLocalBot(input, deps = {}) {
748
- const run = deps.run ?? execFileSync;
749
- const platform = deps.platform ?? process.platform;
750
- const root = deps.fleetHome ?? fleetHome();
751
- const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
752
- const profileDir = profileRoot(input.agentUid, root);
753
- const sources = path.join(profileDir, "sources");
754
- const distributionSource = path.join(sources, "distribution");
755
- const runtimeSource = path.join(sources, "runtime");
756
- const runtimeDir = path.join(profileDir, "runtime");
757
- const marker = path.join(profileDir, "bootstrap.json");
758
- fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
759
- if (!fs.existsSync(marker)) {
760
- fs.rmSync(sources, { recursive: true, force: true });
761
- fs.rmSync(runtimeDir, { recursive: true, force: true });
767
+ async function installSharedFleetRuntime(installation, root, python, platform, run) {
768
+ const identity = digest(Buffer.from(JSON.stringify({
769
+ version: installation.release.version,
770
+ distribution: installation.release.artifactSha256,
771
+ runtime: installation.release.runtimeArtifactSha256,
772
+ python: python.python,
773
+ platform,
774
+ extras: ["mcp", "anthropic"],
775
+ })));
776
+ const shared = path.join(root, "runtimes", identity);
777
+ return withBotLock(identity, async () => {
778
+ const marker = path.join(shared, "ready.json");
779
+ const sources = path.join(shared, "sources");
780
+ const distributionSource = path.join(sources, "distribution");
781
+ const runtimeSource = path.join(sources, "runtime");
782
+ const runtimeDir = path.join(shared, "runtime");
783
+ const executables = ["python", "hq-fleet", "hq-fleet-local"];
784
+ if (fs.existsSync(marker)) {
785
+ if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
786
+ throw expectedError("The shared HQ Fleet Runtime is incomplete. Reinstall it before creating another bot.");
787
+ }
788
+ return shared;
789
+ }
790
+ fs.rmSync(shared, { recursive: true, force: true });
762
791
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
763
792
  fs.mkdirSync(runtimeSource, { recursive: true, mode: 0o700 });
764
793
  const uvEnv = fleetUvEnv(python.pythonDir);
@@ -766,14 +795,14 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
766
795
  "-m",
767
796
  "zipfile",
768
797
  "-e",
769
- input.installation.archive,
798
+ installation.archive,
770
799
  distributionSource,
771
800
  ], { stdio: "ignore" }, "distribution extraction");
772
801
  checkedRun(run, python.python, [
773
802
  "-m",
774
803
  "tarfile",
775
804
  "-e",
776
- input.installation.runtimeArchive,
805
+ installation.runtimeArchive,
777
806
  runtimeSource,
778
807
  ], { stdio: "ignore" }, "runtime extraction");
779
808
  checkedRun(run, python.uv, [
@@ -794,6 +823,29 @@ export async function bootstrapFleetLocalBot(input, deps = {}) {
794
823
  `${runtimeSource}[mcp,anthropic]`,
795
824
  distributionSource,
796
825
  ], { stdio: ["ignore", 2, 2], env: uvEnv }, "package installation");
826
+ if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
827
+ throw expectedError("HQ Fleet Local installed without its runtime executables.");
828
+ }
829
+ fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: installation.engine })}\n`, { mode: 0o600 });
830
+ return shared;
831
+ }, { root: path.join(root, "runtimes"), command: "HQ Fleet Runtime installation", waitMs: 20 * 60_000, staleMs: 2 * 60 * 60_000 });
832
+ }
833
+ export async function bootstrapFleetLocalBot(input, deps = {}) {
834
+ const run = deps.run ?? execFileSync;
835
+ const platform = deps.platform ?? process.platform;
836
+ const root = deps.fleetHome ?? fleetHome();
837
+ const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
838
+ const profileDir = profileRoot(input.agentUid, root);
839
+ const runtimeDir = path.join(profileDir, "runtime");
840
+ const marker = path.join(profileDir, "bootstrap.json");
841
+ fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
842
+ if (!fs.existsSync(marker)) {
843
+ const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run);
844
+ for (const name of ["sources", "runtime"]) {
845
+ const target = path.join(profileDir, name);
846
+ fs.rmSync(target, { recursive: true, force: true });
847
+ fs.symlinkSync(path.join(shared, name), target, platform === "win32" ? "junction" : "dir");
848
+ }
797
849
  fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: input.installation.engine }, null, 2)}\n`, { mode: 0o600 });
798
850
  }
799
851
  const { systemPrompt, ...profileInput } = input;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The bot's built-in hot-signal-sync job.
3
+ *
4
+ * A local bot that is bound to a company registers one recurring job in its
5
+ * own `jobs.json` the first time it starts (`ensureHotSignalSyncJob`). The job
6
+ * fires from the bot's inbox loop like every other local job
7
+ * (lib/bot/local-jobs.ts): no crontab, launchd or systemd entry is written.
8
+ * The default schedule is 06:30 daily in the machine's time zone, ahead of a
9
+ * morning brief; the owner changes it with
10
+ * `hq bot jobs <bot> --reschedule <jobId> --every "<when>"`, or removes it with
11
+ * `--rm`, and a removed job is not added back on the next start.
12
+ *
13
+ * When the job fires the bot runs the sync in-process, with no model turn
14
+ * (`runHotSignalSyncJob`): the same hot-channel sync `hq sources sync` runs,
15
+ * with `trigger: "scheduled"` and `catchUp` set when the run fired late after
16
+ * a missed window. Catch-up is bounded by each adapter's `catchUpWindow`, and
17
+ * per-connection cursors keep a redelivered run from landing anything twice.
18
+ * The claude signals pass (US-011) runs after each adapter when the bot wires
19
+ * one in; with none the signals count is 0.
20
+ */
21
+ import type { HotChannelRunResult, WriteGuard } from "../../hot-channels/loop.js";
22
+ import type { RunTrigger } from "../../hot-channels/ledger.js";
23
+ import type { OnDemandSyncReport } from "../../hot-channels/run.js";
24
+ import { type BotConfig } from "../config.js";
25
+ import { type LocalJob } from "../local-jobs.js";
26
+ export declare const HOT_SIGNAL_SYNC_JOB_KIND = "hot-signal-sync";
27
+ /**
28
+ * 06:30 every day, in the job's time zone (the stored EventBridge form of
29
+ * "30 6 * * *"). Only the first registration uses it; after that the job's
30
+ * own schedule in `jobs.json` is the one that runs.
31
+ */
32
+ export declare const HOT_SIGNAL_SYNC_DEFAULT_CRON = "cron(30 6 * * ? *)";
33
+ /** The private context that marks a job as this built-in, for one company. */
34
+ export declare function hotSignalSyncJobContext(company: string): Record<string, unknown>;
35
+ /** The company a job item syncs, when its context marks it as this built-in; else null. */
36
+ export declare function hotSignalSyncJobCompany(context: unknown): string | null;
37
+ /**
38
+ * The company a bot's hot-signal-sync job syncs: the bot's own company, else
39
+ * the first company it serves. Null = none, and no job is registered. There is
40
+ * no fallback to the HQ's active company: a personal bot syncs a company only
41
+ * when it was given one explicitly (`--company`), so the job never writes into
42
+ * a vault the bot does not belong to. Checked again each time the job fires.
43
+ */
44
+ export declare function currentHotSignalSyncCompany(config: Pick<BotConfig, "companySlug" | "kind" | "companies" | "workerId">): string | null;
45
+ /**
46
+ * Remove every hot-signal-sync job whose company is not `current` (all of them
47
+ * when `current` is null), and forget their registration so the company can be
48
+ * registered again if the bot is given it back. Returns the removed jobs.
49
+ */
50
+ export declare function removeStaleHotSignalSyncJobs(dir: string, current: string | null): LocalJob[];
51
+ export interface EnsureHotSignalSyncJobResult {
52
+ /** The job, or null when the owner removed it after it was first registered. */
53
+ job: LocalJob | null;
54
+ created: boolean;
55
+ }
56
+ /**
57
+ * Register the hot-signal-sync job for `company` once. Safe to call on every
58
+ * bot start: an existing job is left as it is (including a changed schedule),
59
+ * and a job the owner removed is not added again.
60
+ */
61
+ export declare function ensureHotSignalSyncJob(dir: string, input: {
62
+ company: string;
63
+ timezone: string;
64
+ }, now?: Date): EnsureHotSignalSyncJobResult;
65
+ /**
66
+ * The signals pass over one adapter's landed sources: how many signals it
67
+ * generated. The bot wires the local `claude -p` pass
68
+ * (`createClaudeSignalsPass`); `noLocalSignalsPass` is the default when no
69
+ * pass is given.
70
+ */
71
+ export type LocalSignalsPass = (result: HotChannelRunResult, guard?: WriteGuard) => Promise<number>;
72
+ export declare const noLocalSignalsPass: LocalSignalsPass;
73
+ /** One company's hot-channel sync, as `runCompanyHotChannelSync` runs it (minus the HQ root). */
74
+ export type HotSignalSyncFn = (input: {
75
+ company: string;
76
+ trigger: RunTrigger;
77
+ catchUp: boolean;
78
+ signalsGenerated: LocalSignalsPass;
79
+ now: () => Date;
80
+ /** The owner DM for the signals-failure alert (US-012); unset = no alert check. */
81
+ signalsAlert?: {
82
+ sendOwnerDm: (text: string) => Promise<void>;
83
+ log?: (message: string) => void;
84
+ };
85
+ }) => Promise<OnDemandSyncReport>;
86
+ export interface HotSignalSyncRunInput {
87
+ company: string;
88
+ /** The run fired late, after a missed window (job-run.ts `isCatchUpRun`). */
89
+ catchUp: boolean;
90
+ now: () => Date;
91
+ sync: HotSignalSyncFn;
92
+ signalsPass?: LocalSignalsPass;
93
+ /**
94
+ * Sends the signals-failure alert to the owner as its own message, even when
95
+ * the job is quiet. Throws when the message was not delivered.
96
+ */
97
+ sendAlert?: (text: string) => Promise<void>;
98
+ log?: (message: string) => void;
99
+ }
100
+ export interface HotSignalSyncRunOutcome {
101
+ ok: boolean;
102
+ /** Plain report for the owner's DM and the job's run output. */
103
+ text: string;
104
+ report: OnDemandSyncReport | null;
105
+ }
106
+ /** Run the scheduled sync once and describe it in plain words. Never throws. */
107
+ export declare function runHotSignalSyncJob(input: HotSignalSyncRunInput): Promise<HotSignalSyncRunOutcome>;
108
+ export declare function describeSyncReport(company: string, report: OnDemandSyncReport): string;
109
+ //# sourceMappingURL=hot-signal-sync.d.ts.map
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The bot's built-in hot-signal-sync job.
3
+ *
4
+ * A local bot that is bound to a company registers one recurring job in its
5
+ * own `jobs.json` the first time it starts (`ensureHotSignalSyncJob`). The job
6
+ * fires from the bot's inbox loop like every other local job
7
+ * (lib/bot/local-jobs.ts): no crontab, launchd or systemd entry is written.
8
+ * The default schedule is 06:30 daily in the machine's time zone, ahead of a
9
+ * morning brief; the owner changes it with
10
+ * `hq bot jobs <bot> --reschedule <jobId> --every "<when>"`, or removes it with
11
+ * `--rm`, and a removed job is not added back on the next start.
12
+ *
13
+ * When the job fires the bot runs the sync in-process, with no model turn
14
+ * (`runHotSignalSyncJob`): the same hot-channel sync `hq sources sync` runs,
15
+ * with `trigger: "scheduled"` and `catchUp` set when the run fired late after
16
+ * a missed window. Catch-up is bounded by each adapter's `catchUpWindow`, and
17
+ * per-connection cursors keep a redelivered run from landing anything twice.
18
+ * The claude signals pass (US-011) runs after each adapter when the bot wires
19
+ * one in; with none the signals count is 0.
20
+ */
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+ import { JOB_RUN_OPTIONS_CONTEXT_KEY } from "../job-run.js";
24
+ import { effectiveBotCompanies } from "../config.js";
25
+ import { ensureLocalJob, readLocalJobs, removeLocalJobsWhere, withJobsLock } from "../local-jobs.js";
26
+ export const HOT_SIGNAL_SYNC_JOB_KIND = "hot-signal-sync";
27
+ /**
28
+ * 06:30 every day, in the job's time zone (the stored EventBridge form of
29
+ * "30 6 * * *"). Only the first registration uses it; after that the job's
30
+ * own schedule in `jobs.json` is the one that runs.
31
+ */
32
+ export const HOT_SIGNAL_SYNC_DEFAULT_CRON = "cron(30 6 * * ? *)";
33
+ /** Which built-in jobs this bot has registered once, so a removed one stays removed. */
34
+ const BUILTIN_JOBS_FILE = "builtin-jobs.json";
35
+ function markerKey(company) {
36
+ return `${HOT_SIGNAL_SYNC_JOB_KIND}:${company}`;
37
+ }
38
+ function readMarker(dir) {
39
+ try {
40
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, BUILTIN_JOBS_FILE), "utf-8"));
41
+ if (parsed && parsed.v === 1 && parsed.registered && typeof parsed.registered === "object") {
42
+ return { v: 1, registered: { ...parsed.registered } };
43
+ }
44
+ }
45
+ catch {
46
+ // Missing or unreadable: nothing registered yet.
47
+ }
48
+ return { v: 1, registered: {} };
49
+ }
50
+ function writeMarker(dir, marker) {
51
+ const file = path.join(dir, BUILTIN_JOBS_FILE);
52
+ const tmp = `${file}.${process.pid}.tmp`;
53
+ fs.writeFileSync(tmp, JSON.stringify(marker, null, 2) + "\n", { mode: 0o600 });
54
+ fs.renameSync(tmp, file);
55
+ }
56
+ /** The private context that marks a job as this built-in, for one company. */
57
+ export function hotSignalSyncJobContext(company) {
58
+ return { [JOB_RUN_OPTIONS_CONTEXT_KEY]: { builtin: HOT_SIGNAL_SYNC_JOB_KIND, company } };
59
+ }
60
+ /** The company a job item syncs, when its context marks it as this built-in; else null. */
61
+ export function hotSignalSyncJobCompany(context) {
62
+ if (!context || typeof context !== "object" || Array.isArray(context))
63
+ return null;
64
+ const options = context[JOB_RUN_OPTIONS_CONTEXT_KEY];
65
+ if (!options || typeof options !== "object" || Array.isArray(options))
66
+ return null;
67
+ const { builtin, company } = options;
68
+ if (builtin !== HOT_SIGNAL_SYNC_JOB_KIND)
69
+ return null;
70
+ return typeof company === "string" && company.trim() ? company.trim() : null;
71
+ }
72
+ /**
73
+ * The company a bot's hot-signal-sync job syncs: the bot's own company, else
74
+ * the first company it serves. Null = none, and no job is registered. There is
75
+ * no fallback to the HQ's active company: a personal bot syncs a company only
76
+ * when it was given one explicitly (`--company`), so the job never writes into
77
+ * a vault the bot does not belong to. Checked again each time the job fires.
78
+ */
79
+ export function currentHotSignalSyncCompany(config) {
80
+ return config.companySlug ?? effectiveBotCompanies(config)[0] ?? null;
81
+ }
82
+ /**
83
+ * Remove every hot-signal-sync job whose company is not `current` (all of them
84
+ * when `current` is null), and forget their registration so the company can be
85
+ * registered again if the bot is given it back. Returns the removed jobs.
86
+ */
87
+ export function removeStaleHotSignalSyncJobs(dir, current) {
88
+ const removed = removeLocalJobsWhere(dir, (job) => {
89
+ const company = hotSignalSyncJobCompany(job.context);
90
+ return company !== null && company !== current;
91
+ });
92
+ const keep = current ? markerKey(current) : null;
93
+ withJobsLock(dir, () => {
94
+ const marker = readMarker(dir);
95
+ let changed = false;
96
+ for (const key of Object.keys(marker.registered)) {
97
+ if (key.startsWith(HOT_SIGNAL_SYNC_JOB_KIND + ":") && key !== keep) {
98
+ delete marker.registered[key];
99
+ changed = true;
100
+ }
101
+ }
102
+ if (changed)
103
+ writeMarker(dir, marker);
104
+ });
105
+ return removed;
106
+ }
107
+ /**
108
+ * Register the hot-signal-sync job for `company` once. Safe to call on every
109
+ * bot start: an existing job is left as it is (including a changed schedule),
110
+ * and a job the owner removed is not added again.
111
+ */
112
+ export function ensureHotSignalSyncJob(dir, input, now = new Date()) {
113
+ const key = markerKey(input.company);
114
+ const matches = (job) => hotSignalSyncJobCompany(job.context) === input.company;
115
+ if (readMarker(dir).registered[key]) {
116
+ return { job: readLocalJobs(dir).find(matches) ?? null, created: false };
117
+ }
118
+ const { job, created } = ensureLocalJob(dir, matches, {
119
+ prompt: `Sync hot channels for ${input.company} and run the signals pass (runs in the bot, no model turn).`,
120
+ cron: HOT_SIGNAL_SYNC_DEFAULT_CRON,
121
+ timezone: input.timezone,
122
+ context: hotSignalSyncJobContext(input.company),
123
+ }, now);
124
+ withJobsLock(dir, () => {
125
+ const marker = readMarker(dir);
126
+ if (!marker.registered[key]) {
127
+ marker.registered[key] = now.toISOString();
128
+ writeMarker(dir, marker);
129
+ }
130
+ });
131
+ return { job, created };
132
+ }
133
+ export const noLocalSignalsPass = async () => 0;
134
+ /** Run the scheduled sync once and describe it in plain words. Never throws. */
135
+ export async function runHotSignalSyncJob(input) {
136
+ const signalsPass = input.signalsPass ?? noLocalSignalsPass;
137
+ let report;
138
+ try {
139
+ report = await input.sync({
140
+ company: input.company,
141
+ trigger: "scheduled",
142
+ catchUp: input.catchUp,
143
+ signalsGenerated: signalsPass,
144
+ now: input.now,
145
+ ...(input.sendAlert
146
+ ? { signalsAlert: { sendOwnerDm: input.sendAlert, ...(input.log ? { log: input.log } : {}) } }
147
+ : {}),
148
+ });
149
+ }
150
+ catch (err) {
151
+ const reason = err instanceof Error ? err.message : String(err);
152
+ return { ok: false, text: `The hot-channel sync for ${input.company} failed: ${reason}`, report: null };
153
+ }
154
+ return { ok: report.exitCode === 0 && !report.signInRequired, text: describeSyncReport(input.company, report), report };
155
+ }
156
+ export function describeSyncReport(company, report) {
157
+ if (report.signInRequired) {
158
+ return `The hot-channel sync for ${company} did not run: ${report.notices.join(" ")}`;
159
+ }
160
+ if (report.skipped)
161
+ return report.notices.join(" ");
162
+ const landed = report.outcomes.reduce((sum, o) => sum + o.landed, 0);
163
+ const signals = report.entries.reduce((sum, e) => sum + e.signalsGenerated, 0);
164
+ const lines = [
165
+ `Hot-channel sync for ${company}: ${landed} new source${landed === 1 ? "" : "s"}, ${signals} signal${signals === 1 ? "" : "s"}.`,
166
+ ];
167
+ for (const outcome of report.outcomes) {
168
+ if (outcome.status === "ok")
169
+ continue;
170
+ const label = outcome.appName ?? outcome.adapter;
171
+ lines.push(`- ${label}: ${outcome.reconnect?.prompt ?? outcome.message ?? outcome.status}`);
172
+ }
173
+ for (const notice of report.notices)
174
+ lines.push(notice);
175
+ return lines.join("\n");
176
+ }
177
+ //# sourceMappingURL=hot-signal-sync.js.map
@@ -18,7 +18,7 @@
18
18
  * - A fire is recorded before it is handed over, so a crash never runs the
19
19
  * same due time twice.
20
20
  */
21
- import { type BotApi, type InboxItem } from "./api.js";
21
+ import { type BotApi, type BotJobRecord, type InboxItem } from "./api.js";
22
22
  export interface LocalJob {
23
23
  jobId: string;
24
24
  prompt: string;
@@ -27,6 +27,12 @@ export interface LocalJob {
27
27
  timezone: string;
28
28
  context?: Record<string, unknown>;
29
29
  createdAt: string;
30
+ /**
31
+ * When the schedule was last changed (`hq bot jobs --reschedule`). Fire
32
+ * times before it are never treated as missed, so a new schedule does not
33
+ * fire a catch-up for a time that only matches the new expression.
34
+ */
35
+ scheduledFrom?: string;
30
36
  /** The newest due time already handed to the bot. */
31
37
  lastFiredAt?: string;
32
38
  /** The run in progress, if any. */
@@ -72,6 +78,29 @@ export declare function addLocalJob(dir: string, input: {
72
78
  timezone: string;
73
79
  context?: Record<string, unknown>;
74
80
  }, now?: Date): LocalJob;
81
+ /**
82
+ * Add a job unless one already matches, as one locked read-and-write, so two
83
+ * bot starts racing each other still leave a single job.
84
+ */
85
+ export declare function ensureLocalJob(dir: string, matches: (job: LocalJob) => boolean, input: {
86
+ prompt: string;
87
+ cron: string;
88
+ timezone: string;
89
+ context?: Record<string, unknown>;
90
+ }, now?: Date): {
91
+ job: LocalJob;
92
+ created: boolean;
93
+ };
94
+ /**
95
+ * Change a job's schedule in place, keeping its id, instruction, context and
96
+ * run history. Returns null when the bot has no job with that id.
97
+ */
98
+ export declare function rescheduleLocalJob(dir: string, jobId: string, schedule: {
99
+ cron: string;
100
+ timezone: string;
101
+ }, now?: Date): LocalJob | null;
102
+ /** Remove every job `matches` picks, as one locked read-and-write. Returns the removed jobs. */
103
+ export declare function removeLocalJobsWhere(dir: string, matches: (job: LocalJob) => boolean): LocalJob[];
75
104
  /** Forget the once-per-minute check (tests, and a bot restarted in-process). */
76
105
  export declare function resetLocalJobChecks(): void;
77
106
  export declare function takeDueLocalJobRuns(dir: string, now?: Date): InboxItem[];
@@ -81,9 +110,16 @@ export declare function takeDueLocalJobRuns(dir: string, now?: Date): InboxItem[
81
110
  * locally instead of being sent to the cloud.
82
111
  */
83
112
  export declare function withLocalJobs(api: BotApi, dir: string, clock?: () => Date): BotApi;
113
+ /** What `hq bot jobs` uses: list, add, remove, and (local bots) reschedule. */
114
+ export type LocalJobsApi = Pick<BotApi, "listJobs" | "createJob" | "cancelJob"> & {
115
+ rescheduleJob?(agentUid: string, jobId: string, schedule: {
116
+ cron: string;
117
+ timezone: string;
118
+ }): Promise<BotJobRecord>;
119
+ };
84
120
  /**
85
- * `hq bot jobs` against the bot's own folder: the same three calls the cloud
86
- * client offers, answered from `jobs.json`.
121
+ * `hq bot jobs` against the bot's own folder: the same calls the cloud client
122
+ * offers, plus reschedule, answered from `jobs.json`.
87
123
  */
88
- export declare function localJobsClient(dir: string, clock?: () => Date): Pick<BotApi, "listJobs" | "createJob" | "cancelJob">;
124
+ export declare function localJobsClient(dir: string, clock?: () => Date): Required<LocalJobsApi>;
89
125
  //# sourceMappingURL=local-jobs.d.ts.map