@indigoai-us/hq-cli 5.345.178 → 5.345.179

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 (80) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/command-catalog.generated.d.ts +71 -1
  3. package/dist/command-catalog.generated.js +86 -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 +8 -0
  7. package/dist/commands/bot.js +44 -0
  8. package/dist/commands/daemon.d.ts +2 -0
  9. package/dist/commands/daemon.js +15 -4
  10. package/dist/commands/integrations-core.d.ts +2 -0
  11. package/dist/commands/integrations-core.js +1 -0
  12. package/dist/commands/sources.d.ts +89 -0
  13. package/dist/commands/sources.js +363 -1
  14. package/dist/lib/bot/fleet-local.d.ts +1 -1
  15. package/dist/lib/bot/fleet-local.js +7 -7
  16. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
  17. package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
  18. package/dist/lib/bot/local-jobs.d.ts +40 -4
  19. package/dist/lib/bot/local-jobs.js +72 -5
  20. package/dist/lib/bot/run.d.ts +15 -0
  21. package/dist/lib/bot/run.js +61 -0
  22. package/dist/lib/daemon/handoff.d.ts +4 -0
  23. package/dist/lib/daemon/handoff.js +6 -2
  24. package/dist/lib/daemon/install.d.ts +70 -2
  25. package/dist/lib/daemon/install.js +185 -27
  26. package/dist/lib/daemon/legacy.d.ts +9 -3
  27. package/dist/lib/daemon/legacy.js +11 -4
  28. package/dist/lib/daemon/migrate.js +1 -1
  29. package/dist/lib/daemon/run.d.ts +3 -2
  30. package/dist/lib/daemon/run.js +10 -6
  31. package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
  32. package/dist/lib/doctor/checks/anywhere.js +35 -6
  33. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
  34. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
  35. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
  36. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
  37. package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
  38. package/dist/lib/hot-channels/adapters/attio.js +374 -0
  39. package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
  40. package/dist/lib/hot-channels/adapters/gmail.js +413 -0
  41. package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
  42. package/dist/lib/hot-channels/adapters/index.js +39 -0
  43. package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
  44. package/dist/lib/hot-channels/adapters/linear.js +321 -0
  45. package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
  46. package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
  47. package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
  48. package/dist/lib/hot-channels/adapters/meetings.js +523 -0
  49. package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
  50. package/dist/lib/hot-channels/adapters/slack.js +277 -0
  51. package/dist/lib/hot-channels/alert.d.ts +78 -0
  52. package/dist/lib/hot-channels/alert.js +179 -0
  53. package/dist/lib/hot-channels/audience.d.ts +15 -0
  54. package/dist/lib/hot-channels/audience.js +25 -0
  55. package/dist/lib/hot-channels/contract.d.ts +140 -0
  56. package/dist/lib/hot-channels/contract.js +20 -0
  57. package/dist/lib/hot-channels/cursors.d.ts +41 -0
  58. package/dist/lib/hot-channels/cursors.js +84 -0
  59. package/dist/lib/hot-channels/index.d.ts +10 -0
  60. package/dist/lib/hot-channels/index.js +10 -0
  61. package/dist/lib/hot-channels/landing.d.ts +83 -0
  62. package/dist/lib/hot-channels/landing.js +175 -0
  63. package/dist/lib/hot-channels/ledger.d.ts +101 -0
  64. package/dist/lib/hot-channels/ledger.js +186 -0
  65. package/dist/lib/hot-channels/loop.d.ts +74 -0
  66. package/dist/lib/hot-channels/loop.js +219 -0
  67. package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
  68. package/dist/lib/hot-channels/registry.d.ts +28 -0
  69. package/dist/lib/hot-channels/registry.js +121 -0
  70. package/dist/lib/hot-channels/run.d.ts +137 -0
  71. package/dist/lib/hot-channels/run.js +324 -0
  72. package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
  73. package/dist/lib/hot-channels/signals-pass.js +581 -0
  74. package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
  75. package/dist/lib/hot-channels/sync-lock.js +224 -0
  76. package/dist/lib/install/hook-target.d.ts +6 -2
  77. package/dist/lib/install/hook-target.js +9 -4
  78. package/dist/lib/lanes/loop.d.ts +10 -0
  79. package/dist/lib/lanes/loop.js +18 -1
  80. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.179] — 2026-10-29
6
+ - Conduct loop lanes now count a phase's absolute `deadline` from when the lane starts the phase, so time the envelope spent waiting in the queue no longer eats its run budget. An unparsable `deadline` now falls back to the phase's normal budget instead of an invalid one.
7
+ - Pins hq-fleet-local v0.4.13 so a personal bot's first-start kickoff waits for HQ Fleet Runtime to attach before delivery instead of being dropped during startup.
8
+ - `hq sources sync` runs every hot channel now for the signed-in person: each adapter once per integration connection the person created or was granted, through the HQ integration gateway. It prints sources landed per adapter and connection. A signed-out session or a revoked connection prints a reconnect prompt naming the connection, exits non-zero, and leaves the cursor unchanged. Use `--adapter <id>` to run one adapter and `--company <slug>` to pick the company. Catalog channels with no adapter in this build report "adapter not available in this hq version".
9
+ - `hq sources runs --today` shows the hot-channel run log for the signed-in person: per adapter, the sources each run landed and the signals it generated, with start and finish times and any error. Cursors live at `sources/_cursors/@{personKey}/` and each run appends to `sources/_runs/@{personKey}/{day}.jsonl`, where the person key is a short hash of the person's email.
10
+ - `hq bot run` registers a built-in hot-channel sync job for the bot's company: daily at 06:30 in the computer's time zone, run inside the bot with no model turn. If the computer was asleep at 06:30, the job runs once when it wakes and the ledger records `catchUp: true`. No crontab or launchd entry is written. Change the time with the new `hq bot jobs <bot> --reschedule <jobId> --every "..."` (or `--cron`); a removed job is not added back.
11
+ - Only one hot-channel sync runs at a time per person and company. If the bot's scheduled run and `hq sources sync` overlap, the second one lands nothing, says another sync is running, and writes a `skipped` line to the run log.
12
+ - Linear: issues assigned to you that changed since the last sync land as source documents.
13
+ - Slack: threads HQ has saved to your company vault that you are part of become one signal each, pointing at the thread with no message text copied. A Slack rate-limit pause is reported with a prompt to retry or reconnect.
14
+ - Attio: activity HQ has saved to your company vault becomes signals only when it is about you (changes you made, records you own, comments or notes that mention or are assigned to you), with no payload copied.
15
+ - Meetings: transcripts already in `sources/meetings/` that you attended land as short pointer documents under `sources/meeting-events/`, with no transcript text copied.
16
+ - Gmail ships behind the `hq.hot-channel-gmail` flag. While the flag is off, Gmail is listed as `gated` and never called. With it on, new INBOX mail lands as header-only pointers (never the body or snippet) with a read-only scope.
17
+ - Newly landed sources are turned into signals with your own logged-in `claude`: up to 20 documents per run, each signal linked to its source and listed in the day's signal index. Signals from meetings, Slack and Attio use the full saved content each pointer names (up to 200 KB). A logged-out `claude`, a timeout, bad output, or any other error in one adapter's signals step marks that adapter failed in the run log and the run continues with the next adapter.
18
+ - One malformed event from a channel is skipped and counted as rejected in the run log, with the reason, instead of stopping that channel on every later run.
19
+ - Hot-channel syncs alert when sources land but no signals come out: the bot DMs you and `hq sources sync` prints the alert, at most once an hour per connection. Set `signals_alert_channel: <Slack channel id>` in the company's `settings/signals/preferences.yaml` to also post it to Slack.
20
+ - Contributors: adding a hot channel is one adapter file plus one catalog entry, with end-to-end tests for both run triggers.
21
+ - On Windows, `hq daemon install` now registers a scheduled task named for the current user (`ai.getindigo.hq-daemon-<SID>`, or a sanitised username when the SID cannot be read), so two people who share a PC no longer overwrite each other's daemon. Status, uninstall, `hq doctor` and the handoff and legacy lookups use the same per-user name. An older shared `ai.getindigo.hq-daemon` task that runs as the current user is moved to the new name and kept running; one that belongs to another Windows user, or whose owner cannot be read, is left untouched and `hq` says so.
22
+ - `hq daemon uninstall` on Windows now works for users who upgraded from the shared task and have not re-run install: it checks schtasks for the per-user task instead of trusting the shared XML file, treats a per-user task that is already gone as removed, and also removes this user's task registered under the username form. A task owner stored as a bare user name is no longer treated as `DOMAIN\user`, so another domain's user of the same name keeps their task.
23
+
5
24
  ## [5.345.178] — 2026-10-29
6
25
  - `hq dm send --attach` now uploads the bytes it read with their exact length, so a file changed after the size check no longer stalls and fails with `Upload failed (HTTP 400)`. A refused upload now names the storage error (for example `RequestTimeout`).
7
26
  - `hq daemon disable mesh` now keeps `mesh` off on machines where it is not on by default,
@@ -5150,6 +5150,72 @@ export declare const COMMAND_CATALOG: readonly [{
5150
5150
  readonly hidden: false;
5151
5151
  }];
5152
5152
  readonly subcommands: readonly [];
5153
+ }, {
5154
+ readonly name: "runs";
5155
+ readonly description: "Show the hot-channel run log: sources landed and signals generated per adapter";
5156
+ readonly aliases: readonly [];
5157
+ readonly hidden: false;
5158
+ readonly helpOption: true;
5159
+ readonly helpCommand: false;
5160
+ readonly usage: "[options]";
5161
+ readonly helpText: readonly [];
5162
+ readonly arguments: readonly [];
5163
+ readonly options: readonly [{
5164
+ readonly flags: "--today";
5165
+ readonly description: "Show today's runs (UTC); the default";
5166
+ readonly hidden: false;
5167
+ }, {
5168
+ readonly flags: "--date <YYYY-MM-DD>";
5169
+ readonly description: "Show runs for this UTC day";
5170
+ readonly hidden: false;
5171
+ }, {
5172
+ readonly flags: "--company <slug>";
5173
+ readonly description: "Company slug (defaults to .hq/config.json activeCompany)";
5174
+ readonly hidden: false;
5175
+ }, {
5176
+ readonly flags: "--email <address>";
5177
+ readonly description: "Whose runs to show (defaults to the signed-in person)";
5178
+ readonly hidden: false;
5179
+ }, {
5180
+ readonly flags: "--format <fmt>";
5181
+ readonly description: "Output format: table | json (default: table for TTY, json piped)";
5182
+ readonly hidden: false;
5183
+ }, {
5184
+ readonly flags: "--hq-root <path>";
5185
+ readonly description: "Local HQ tree root (default: <runtime-path>)";
5186
+ readonly hidden: false;
5187
+ readonly defaultValue: "<runtime-path>";
5188
+ }];
5189
+ readonly subcommands: readonly [];
5190
+ }, {
5191
+ readonly name: "sync";
5192
+ readonly description: "Run every hot channel now: each adapter once per connection you can use";
5193
+ readonly aliases: readonly [];
5194
+ readonly hidden: false;
5195
+ readonly helpOption: true;
5196
+ readonly helpCommand: false;
5197
+ readonly usage: "[options]";
5198
+ readonly helpText: readonly [];
5199
+ readonly arguments: readonly [];
5200
+ readonly options: readonly [{
5201
+ readonly flags: "--adapter <id>";
5202
+ readonly description: "Run one adapter only (a catalog provider or adapter id)";
5203
+ readonly hidden: false;
5204
+ }, {
5205
+ readonly flags: "--company <slug>";
5206
+ readonly description: "Company slug (defaults to .hq/config.json activeCompany)";
5207
+ readonly hidden: false;
5208
+ }, {
5209
+ readonly flags: "--json";
5210
+ readonly description: "Print the run report as JSON";
5211
+ readonly hidden: false;
5212
+ }, {
5213
+ readonly flags: "--hq-root <path>";
5214
+ readonly description: "Local HQ tree root (default: <runtime-path>)";
5215
+ readonly hidden: false;
5216
+ readonly defaultValue: "<runtime-path>";
5217
+ }];
5218
+ readonly subcommands: readonly [];
5153
5219
  }];
5154
5220
  }, {
5155
5221
  readonly name: "signals";
@@ -18691,7 +18757,7 @@ export declare const COMMAND_CATALOG: readonly [{
18691
18757
  readonly subcommands: readonly [];
18692
18758
  }, {
18693
18759
  readonly name: "jobs";
18694
- readonly description: "Show, schedule, or remove a bot's recurring jobs (the schedule runs in the cloud, not on this computer)";
18760
+ readonly description: "Show, schedule, reschedule, or remove a bot's recurring jobs (they run on this computer while the bot runs)";
18695
18761
  readonly aliases: readonly [];
18696
18762
  readonly hidden: false;
18697
18763
  readonly helpOption: true;
@@ -18728,6 +18794,10 @@ export declare const COMMAND_CATALOG: readonly [{
18728
18794
  readonly flags: "--rm <jobId>";
18729
18795
  readonly description: "Remove a scheduled job";
18730
18796
  readonly hidden: false;
18797
+ }, {
18798
+ readonly flags: "--reschedule <jobId>";
18799
+ readonly description: "Change a job's schedule (with --every or --cron)";
18800
+ readonly hidden: false;
18731
18801
  }, {
18732
18802
  readonly flags: "--json";
18733
18803
  readonly description: "Print machine-readable JSON";
@@ -6243,6 +6243,86 @@ export const COMMAND_CATALOG = [
6243
6243
  }
6244
6244
  ],
6245
6245
  "subcommands": []
6246
+ },
6247
+ {
6248
+ "name": "runs",
6249
+ "description": "Show the hot-channel run log: sources landed and signals generated per adapter",
6250
+ "aliases": [],
6251
+ "hidden": false,
6252
+ "helpOption": true,
6253
+ "helpCommand": false,
6254
+ "usage": "[options]",
6255
+ "helpText": [],
6256
+ "arguments": [],
6257
+ "options": [
6258
+ {
6259
+ "flags": "--today",
6260
+ "description": "Show today's runs (UTC); the default",
6261
+ "hidden": false
6262
+ },
6263
+ {
6264
+ "flags": "--date <YYYY-MM-DD>",
6265
+ "description": "Show runs for this UTC day",
6266
+ "hidden": false
6267
+ },
6268
+ {
6269
+ "flags": "--company <slug>",
6270
+ "description": "Company slug (defaults to .hq/config.json activeCompany)",
6271
+ "hidden": false
6272
+ },
6273
+ {
6274
+ "flags": "--email <address>",
6275
+ "description": "Whose runs to show (defaults to the signed-in person)",
6276
+ "hidden": false
6277
+ },
6278
+ {
6279
+ "flags": "--format <fmt>",
6280
+ "description": "Output format: table | json (default: table for TTY, json piped)",
6281
+ "hidden": false
6282
+ },
6283
+ {
6284
+ "flags": "--hq-root <path>",
6285
+ "description": "Local HQ tree root (default: <runtime-path>)",
6286
+ "hidden": false,
6287
+ "defaultValue": "<runtime-path>"
6288
+ }
6289
+ ],
6290
+ "subcommands": []
6291
+ },
6292
+ {
6293
+ "name": "sync",
6294
+ "description": "Run every hot channel now: each adapter once per connection you can use",
6295
+ "aliases": [],
6296
+ "hidden": false,
6297
+ "helpOption": true,
6298
+ "helpCommand": false,
6299
+ "usage": "[options]",
6300
+ "helpText": [],
6301
+ "arguments": [],
6302
+ "options": [
6303
+ {
6304
+ "flags": "--adapter <id>",
6305
+ "description": "Run one adapter only (a catalog provider or adapter id)",
6306
+ "hidden": false
6307
+ },
6308
+ {
6309
+ "flags": "--company <slug>",
6310
+ "description": "Company slug (defaults to .hq/config.json activeCompany)",
6311
+ "hidden": false
6312
+ },
6313
+ {
6314
+ "flags": "--json",
6315
+ "description": "Print the run report as JSON",
6316
+ "hidden": false
6317
+ },
6318
+ {
6319
+ "flags": "--hq-root <path>",
6320
+ "description": "Local HQ tree root (default: <runtime-path>)",
6321
+ "hidden": false,
6322
+ "defaultValue": "<runtime-path>"
6323
+ }
6324
+ ],
6325
+ "subcommands": []
6246
6326
  }
6247
6327
  ]
6248
6328
  },
@@ -22774,7 +22854,7 @@ export const COMMAND_CATALOG = [
22774
22854
  },
22775
22855
  {
22776
22856
  "name": "jobs",
22777
- "description": "Show, schedule, or remove a bot's recurring jobs (the schedule runs in the cloud, not on this computer)",
22857
+ "description": "Show, schedule, reschedule, or remove a bot's recurring jobs (they run on this computer while the bot runs)",
22778
22858
  "aliases": [],
22779
22859
  "hidden": false,
22780
22860
  "helpOption": true,
@@ -22820,6 +22900,11 @@ export const COMMAND_CATALOG = [
22820
22900
  "description": "Remove a scheduled job",
22821
22901
  "hidden": false
22822
22902
  },
22903
+ {
22904
+ "flags": "--reschedule <jobId>",
22905
+ "description": "Change a job's schedule (with --every or --cron)",
22906
+ "hidden": false
22907
+ },
22823
22908
  {
22824
22909
  "flags": "--json",
22825
22910
  "description": "Print machine-readable JSON",
@@ -5,17 +5,17 @@
5
5
  * hq bot jobs <botName> --add "<what to do>" --every "<plain english>"
6
6
  * hq bot jobs <botName> --add "<what to do>" --cron "0 9 * * 1-5"
7
7
  * hq bot jobs <botName> --rm <jobId>
8
+ * hq bot jobs <botName> --reschedule <jobId> --every "<plain english>"
8
9
  *
9
- * The schedule lives in the cloud, never on this computer: hq-pro-agents holds
10
- * the job and fires it on an EventBridge schedule, and the bot's inbox loop
11
- * runs the fired run (see lib/bot/job-run.ts). Nothing here writes a crontab,
12
- * a launchd plist, or a systemd unit — by rule.
13
- *
14
- * The routes are self-only per agent, so every call uses the BOT's own machine
15
- * identity — the same credential path its turns use — not the owner's session.
10
+ * The schedule lives with the bot: its jobs sit in the bot's own folder
11
+ * (lib/bot/local-jobs.ts) and the bot's inbox loop fires them while it runs
12
+ * (see lib/bot/job-run.ts). Nothing here writes a crontab, a launchd plist, or
13
+ * a systemd unit — by rule. Schedules are still written in the EventBridge form
14
+ * and held to the same limits, so a job reads the same wherever it runs.
16
15
  */
17
16
  import type { Command } from "commander";
18
- import { BotApi, type BotJobRecord, type CreateJobInput } from "../lib/bot/api.js";
17
+ import { type LocalJobsApi } from "../lib/bot/local-jobs.js";
18
+ import { type BotJobRecord, type CreateJobInput } from "../lib/bot/api.js";
19
19
  import { type BotConfig, type BotKind } from "../lib/bot/config.js";
20
20
  /** Mirrors hq-pro-agents `MIN_RECURRING_INTERVAL_SECONDS`. */
21
21
  export declare const MIN_RECURRING_INTERVAL_SECONDS: number;
@@ -100,7 +100,7 @@ export interface BotJobsDeps {
100
100
  };
101
101
  botTokenSupplier(dir: string): () => Promise<string>;
102
102
  /** Test seam: the jobs client for a bot directory. */
103
- apiFor?(dir: string): Pick<BotApi, "listJobs" | "createJob" | "cancelJob">;
103
+ apiFor?(dir: string): LocalJobsApi;
104
104
  parseSchedule?: typeof parseScheduleText;
105
105
  timezone?: () => string;
106
106
  out?: (line: string) => void;
@@ -113,6 +113,8 @@ export interface BotJobsOptions {
113
113
  cron?: string;
114
114
  tz?: string;
115
115
  rm?: string;
116
+ /** Change an existing job's schedule, keeping its id, instruction and history. */
117
+ reschedule?: string;
116
118
  json?: boolean;
117
119
  quiet?: boolean;
118
120
  }
@@ -5,14 +5,13 @@
5
5
  * hq bot jobs <botName> --add "<what to do>" --every "<plain english>"
6
6
  * hq bot jobs <botName> --add "<what to do>" --cron "0 9 * * 1-5"
7
7
  * hq bot jobs <botName> --rm <jobId>
8
+ * hq bot jobs <botName> --reschedule <jobId> --every "<plain english>"
8
9
  *
9
- * The schedule lives in the cloud, never on this computer: hq-pro-agents holds
10
- * the job and fires it on an EventBridge schedule, and the bot's inbox loop
11
- * runs the fired run (see lib/bot/job-run.ts). Nothing here writes a crontab,
12
- * a launchd plist, or a systemd unit — by rule.
13
- *
14
- * The routes are self-only per agent, so every call uses the BOT's own machine
15
- * identity — the same credential path its turns use — not the owner's session.
10
+ * The schedule lives with the bot: its jobs sit in the bot's own folder
11
+ * (lib/bot/local-jobs.ts) and the bot's inbox loop fires them while it runs
12
+ * (see lib/bot/job-run.ts). Nothing here writes a crontab, a launchd plist, or
13
+ * a systemd unit — by rule. Schedules are still written in the EventBridge form
14
+ * and held to the same limits, so a job reads the same wherever it runs.
16
15
  */
17
16
  import chalk from "chalk";
18
17
  import { localJobsClient } from "../lib/bot/local-jobs.js";
@@ -365,12 +364,56 @@ export async function runBotJobs(nameArg, opts, deps) {
365
364
  client = deps.apiFor ? deps.apiFor(dir) : localJobsClient(dir);
366
365
  return client;
367
366
  };
368
- if (opts.add !== undefined && opts.rm !== undefined) {
369
- refuse("Add a job or remove one, not both in the same command.");
367
+ if ([opts.add, opts.rm, opts.reschedule].filter((value) => value !== undefined).length > 1) {
368
+ refuse("Add, remove, or reschedule one job at a time, not several in the same command.");
370
369
  }
371
370
  if (opts.quiet && opts.add === undefined) {
372
371
  refuse("Use --quiet with --add to set a scheduled job's delivery option.");
373
372
  }
373
+ // Shared by --add and --reschedule: one schedule from --every or --cron, in
374
+ // the stored form, held to the same floor.
375
+ const scheduleFromOptions = () => {
376
+ if (!opts.every && !opts.cron) {
377
+ refuse(`Say how often it should run: --every "every weekday at 9am", or --cron "0 9 * * 1-5".`);
378
+ }
379
+ if (opts.every && opts.cron)
380
+ refuse("Use --every or --cron, not both.");
381
+ const timezone = opts.tz?.trim() || (deps.timezone ?? machineTimezone)();
382
+ // An unknown zone would throw RangeError on every inbox poll; refuse it now.
383
+ try {
384
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
385
+ }
386
+ catch {
387
+ refuse(`"${timezone}" is not a known IANA time zone (for example America/New_York or Europe/London).`);
388
+ }
389
+ const parse = deps.parseSchedule ?? parseScheduleText;
390
+ return opts.every ? parse(opts.every, { timezone }) : { cron: opts.cron, timezone };
391
+ };
392
+ if (opts.reschedule !== undefined) {
393
+ const jobId = opts.reschedule.trim();
394
+ if (!jobId)
395
+ refuse("Say which job to reschedule: --reschedule <jobId>");
396
+ const schedule = scheduleFromOptions();
397
+ const cron = toEventBridgeCron(schedule.cron);
398
+ assertScheduleWithinFloor(cron);
399
+ const api = await jobsApi();
400
+ if (!api.rescheduleJob)
401
+ refuse(`${name}'s jobs cannot be rescheduled in place. Remove the job and add it again.`);
402
+ let updated;
403
+ try {
404
+ updated = await api.rescheduleJob(config.agentUid, jobId, { cron, timezone: schedule.timezone });
405
+ }
406
+ catch (err) {
407
+ refuse(jobsErrorMessage(err, ctx));
408
+ }
409
+ if (opts.json) {
410
+ out(JSON.stringify({ ok: true, job: updated }, null, 2));
411
+ return;
412
+ }
413
+ const words = schedule.human || describeSchedule(updated);
414
+ out(chalk.green(`Rescheduled ${updated.jobId}: ${name} now runs it ${words}.`));
415
+ return;
416
+ }
374
417
  if (opts.rm !== undefined) {
375
418
  const jobId = opts.rm.trim();
376
419
  if (!jobId)
@@ -389,16 +432,7 @@ export async function runBotJobs(nameArg, opts, deps) {
389
432
  if (opts.quiet && !(await quietOptionEnabled())) {
390
433
  refuse(`--quiet requires the ${BOT_JOB_QUIET_OPTION_FLAG} flag, which is currently disabled.`);
391
434
  }
392
- if (!opts.every && !opts.cron) {
393
- refuse(`Say how often it should run: --every "every weekday at 9am", or --cron "0 9 * * 1-5".`);
394
- }
395
- if (opts.every && opts.cron)
396
- refuse("Use --every or --cron, not both.");
397
- const timezone = opts.tz?.trim() || (deps.timezone ?? machineTimezone)();
398
- const parse = deps.parseSchedule ?? parseScheduleText;
399
- const schedule = opts.every
400
- ? parse(opts.every, { timezone })
401
- : { cron: opts.cron, timezone };
435
+ const schedule = scheduleFromOptions();
402
436
  const body = buildCreateJobBody({ prompt: opts.add, cron: schedule.cron, timezone: schedule.timezone, quiet: opts.quiet });
403
437
  const api = await jobsApi();
404
438
  let created;
@@ -412,7 +446,7 @@ export async function runBotJobs(nameArg, opts, deps) {
412
446
  out(JSON.stringify({ ok: true, job: created }, null, 2));
413
447
  return;
414
448
  }
415
- const words = ("human" in schedule && schedule.human) || describeSchedule(created);
449
+ const words = schedule.human || describeSchedule(created);
416
450
  out(chalk.green(`Scheduled ${created.jobId}: ${name} will ${body.prompt.replace(/\s+/g, " ").slice(0, 120)} — ${words}.`));
417
451
  out(chalk.gray(`It runs on this computer while ${name} is running. If a run is due while the computer is asleep or ` +
418
452
  `${name} is stopped, it runs once, as a catch-up, the next time ${name} is running.`));
@@ -432,13 +466,14 @@ export async function runBotJobs(nameArg, opts, deps) {
432
466
  export function registerBotJobsCommand(bot, deps) {
433
467
  bot
434
468
  .command("jobs <name>")
435
- .description("Show, schedule, or remove a bot's recurring jobs (the schedule runs in the cloud, not on this computer)")
469
+ .description("Show, schedule, reschedule, or remove a bot's recurring jobs (they run on this computer while the bot runs)")
436
470
  .option("--add <what>", "Schedule new work, in plain language")
437
471
  .option("--every <when>", 'How often, in plain language (e.g. "every weekday at 9am")')
438
472
  .option("--cron <expr>", 'How often, as a cron expression (e.g. "0 9 * * 1-5")')
439
473
  .option("--tz <zone>", "IANA timezone for the schedule (default: this computer's)")
440
474
  .option("--quiet", "Skip owner DMs after successful runs (requires rollout flag)")
441
475
  .option("--rm <jobId>", "Remove a scheduled job")
476
+ .option("--reschedule <jobId>", "Change a job's schedule (with --every or --cron)")
442
477
  .option("--json", "Print machine-readable JSON")
443
478
  .action(async (name, opts) => {
444
479
  try {
@@ -379,4 +379,12 @@ export interface RemoteBotRow {
379
379
  ownerUid: string | null;
380
380
  }
381
381
  export declare function registerBotCommand(program: Command): void;
382
+ /**
383
+ * The company a bot's hot-signal-sync job syncs: the bot's own company, else
384
+ * the first company it serves. Null = none, and no job is registered. There is
385
+ * no fallback to the HQ's active company: a personal bot syncs a company only
386
+ * when it was given one explicitly (`--company`), so the job never writes into
387
+ * a vault the bot does not belong to.
388
+ */
389
+ export declare function hotSignalSyncCompany(config: Pick<BotConfig, "companySlug" | "kind" | "companies" | "workerId">): string | null;
382
390
  //# sourceMappingURL=bot.d.ts.map
@@ -1,4 +1,8 @@
1
1
  import { withLocalJobs } from "../lib/bot/local-jobs.js";
2
+ import { currentHotSignalSyncCompany, ensureHotSignalSyncJob, removeStaleHotSignalSyncJobs, runHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
3
+ import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
4
+ import { machineTimeZone } from "../lib/bot/job-run.js";
5
+ import { runCompanyHotChannelSync } from "./sources.js";
2
6
  import { OwnerContextCache, lookupOwnerContext } from "../lib/bot/owner-context.js";
3
7
  import { hasActivePromotion } from "../lib/bot/promotion-receipt.js";
4
8
  import { readInflightTurns } from "../lib/bot/inflight.js";
@@ -1274,6 +1278,27 @@ async function runBotResident(nameArg) {
1274
1278
  const token = botTokenSupplier(dir);
1275
1279
  // The bot's own schedule lives in its folder and fires from this loop.
1276
1280
  const api = withLocalJobs(new BotApi({ token }), dir);
1281
+ // The built-in hot-signal-sync job, registered once in the bot's own
1282
+ // jobs.json (06:30 daily in this computer's time zone until the owner
1283
+ // reschedules it with `hq bot jobs`). It runs in-process, with no model turn.
1284
+ const syncCompany = hotSignalSyncCompany(config);
1285
+ try {
1286
+ // A job for a company the bot no longer serves is removed before anything runs.
1287
+ for (const stale of removeStaleHotSignalSyncJobs(dir, syncCompany)) {
1288
+ console.log(`Removed the hot-channel sync job ${stale.jobId}: this bot no longer syncs that company.`);
1289
+ }
1290
+ }
1291
+ catch (err) {
1292
+ console.error(`Could not check the hot-channel sync jobs: ${err instanceof Error ? err.message : String(err)}`);
1293
+ }
1294
+ if (syncCompany) {
1295
+ try {
1296
+ ensureHotSignalSyncJob(dir, { company: syncCompany, timezone: machineTimeZone() });
1297
+ }
1298
+ catch (err) {
1299
+ console.error(`Could not register the hot-channel sync job for ${syncCompany}: ${err instanceof Error ? err.message : String(err)}`);
1300
+ }
1301
+ }
1277
1302
  const owner = new OwnerContextCache(() => lookupOwnerContext(config.ownerUid), Date.now, undefined, config.ownerUid);
1278
1303
  const handle = await runBot({
1279
1304
  dir,
@@ -1281,6 +1306,15 @@ async function runBotResident(nameArg) {
1281
1306
  api,
1282
1307
  runtime: runtimeFor(config.runtime),
1283
1308
  ownerContext: () => owner.get(),
1309
+ hotSignalSync: ({ company, catchUp, now, sendAlert }) => runHotSignalSyncJob({
1310
+ company,
1311
+ catchUp,
1312
+ now,
1313
+ sendAlert,
1314
+ sync: (input) => runCompanyHotChannelSync({ hqRoot: config.hqRoot, ...input }),
1315
+ // A new pass per run: the login preflight and the 20-document cap are per run.
1316
+ signalsPass: createClaudeSignalsPass({ hqRoot: config.hqRoot, company }),
1317
+ }),
1284
1318
  // US-005: decision picks in the owner DM, with the bot's own machine token
1285
1319
  // (never the owner's login).
1286
1320
  decisions: new DecisionsClient(token),
@@ -2641,4 +2675,14 @@ export function registerBotCommand(program) {
2641
2675
  console.log(opts.json ? JSON.stringify({ ok: true, ...r }, null, 2) : r.message);
2642
2676
  })());
2643
2677
  }
2678
+ /**
2679
+ * The company a bot's hot-signal-sync job syncs: the bot's own company, else
2680
+ * the first company it serves. Null = none, and no job is registered. There is
2681
+ * no fallback to the HQ's active company: a personal bot syncs a company only
2682
+ * when it was given one explicitly (`--company`), so the job never writes into
2683
+ * a vault the bot does not belong to.
2684
+ */
2685
+ export function hotSignalSyncCompany(config) {
2686
+ return currentHotSignalSyncCompany(config);
2687
+ }
2644
2688
  //# sourceMappingURL=bot.js.map
@@ -59,6 +59,8 @@ export interface DaemonCommandDeps {
59
59
  /** Reads stdin for the one-shot daemon machine login; never reads env or argv credentials. */
60
60
  readStdin?: () => Promise<string>;
61
61
  windowsUser?: string;
62
+ /** The Windows user's SID; the daemon's Scheduled Task name carries it so each user on a PC has their own task. */
63
+ windowsSid?: string;
62
64
  windowsHeadlessLauncher?: string;
63
65
  systemUnitDir?: string;
64
66
  systemRecordPath?: string;
@@ -24,7 +24,7 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
24
24
  import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
25
25
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
26
26
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
27
- import { DAEMON_WINDOWS_TASK, ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, windowsInteractiveSession, } from "../lib/daemon/install.js";
27
+ import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, legacyWindowsTask, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
28
28
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
29
29
  import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
30
30
  import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
@@ -132,7 +132,10 @@ async function loadDefaults(over) {
132
132
  existsSync: fs.existsSync,
133
133
  listDir: (dir) => fs.readdirSync(dir),
134
134
  launchdJobLoaded: (label) => run("launchctl", ["print", `gui/${process.getuid?.() ?? 0}/${label}`]).status === 0,
135
- windowsTaskExists: (task) => run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
135
+ windowsTaskOwner: (task) => classifyWindowsTask(run, task, {
136
+ windowsSid: over.windowsSid ?? readWindowsSid(run),
137
+ windowsUser: over.windowsUser ?? mesh.resolveWindowsUser(env),
138
+ }).state,
136
139
  })),
137
140
  resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
138
141
  userHome: over.userHome ?? ((user) => loginHome(user, run)),
@@ -140,6 +143,7 @@ async function loadDefaults(over) {
140
143
  sleep: over.sleep,
141
144
  interactiveSession: over.interactiveSession,
142
145
  windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
146
+ windowsSid: over.windowsSid ?? (platform === "win32" ? readWindowsSid(run) : undefined),
143
147
  windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
144
148
  systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
145
149
  syncModeClient: over.syncModeClient,
@@ -196,6 +200,7 @@ function hostFor(d, paths, pathEnv) {
196
200
  uid: d.uid,
197
201
  isRoot: d.isRoot,
198
202
  windowsUser: d.windowsUser ?? "",
203
+ windowsSid: d.windowsSid,
199
204
  windowsHeadlessLauncher: d.windowsHeadlessLauncher,
200
205
  systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
201
206
  env,
@@ -306,6 +311,8 @@ async function runInstall(d, opts) {
306
311
  runningDaemon: () => liveDaemonLock(paths.lockPath, d.daemonAlive),
307
312
  });
308
313
  handOver(d, { paths, detected, runAsUser });
314
+ if (result.notice)
315
+ d.out(result.notice);
309
316
  if (result.warning)
310
317
  d.err(result.warning);
311
318
  if (result.kind === "manual") {
@@ -333,7 +340,7 @@ async function runInstall(d, opts) {
333
340
  ? `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds although you are signed in to Windows.`
334
341
  : `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds, and HQ could not check whether you are signed in to Windows.`,
335
342
  "Check it with: hq daemon status",
336
- `The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${DAEMON_WINDOWS_TASK}').LastTaskResult"`,
343
+ `The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" })}').LastTaskResult"`,
337
344
  ].join("\n"));
338
345
  }
339
346
  if (!result.running) {
@@ -386,6 +393,8 @@ async function runUninstall(d, opts) {
386
393
  }
387
394
  const result = uninstallDaemonUnit(hostFor(d, paths, false), { scope: installScopeFor(detected.type) }, { run: d.run });
388
395
  d.out(result.removed ? `Removed ${result.unitPath}` : "hq daemon was not installed.");
396
+ if (result.notice)
397
+ d.out(result.notice);
389
398
  if (result.warning)
390
399
  d.err(result.warning);
391
400
  if (opts.restoreLegacy) {
@@ -869,7 +878,9 @@ async function runMigrate(d, opts) {
869
878
  platform: d.platform,
870
879
  home: d.home,
871
880
  systemUnitDir: d.systemUnitDir,
881
+ windowsTaskName: windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }),
872
882
  windowsTaskExists: (task) => d.run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
883
+ windowsLegacyTaskIsMine: () => legacyWindowsTask(d.run, { windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }).state === "mine",
873
884
  });
874
885
  if (!installed) {
875
886
  throw new DaemonCommandError("hq daemon is not installed here yet. Run `hq daemon install` first, then `hq daemon migrate --apply`.");
@@ -893,7 +904,7 @@ async function runMigrate(d, opts) {
893
904
  if (u.migratable)
894
905
  d.out(`Would take over ${u.unitId} from ${describeLegacyUnit(u)}${u.path ? ` (${u.path})` : ""}`);
895
906
  else
896
- d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (owned by the HQ desktop app)`);
907
+ d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (${u.notMineReason ?? "owned by the HQ desktop app"})`);
897
908
  }
898
909
  d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
899
910
  }
@@ -299,6 +299,8 @@ export interface UsableApp {
299
299
  * provider, else `--connection <id>`.
300
300
  */
301
301
  selector: string;
302
+ /** The caller created this connection (`viewerAccess.isCreator`). */
303
+ ownedByCaller: boolean;
302
304
  }
303
305
  export interface UsableAppsResult {
304
306
  companyUid: string;
@@ -529,6 +529,7 @@ export async function fetchUsableApps(token, companyUid) {
529
529
  status: row.status,
530
530
  providerArg,
531
531
  selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
532
+ ownedByCaller: row.viewerAccess?.isCreator === true,
532
533
  };
533
534
  })
534
535
  .sort((a, b) => compareNames(a.name, b.name))