@indigoai-us/hq-cli 5.345.191 → 5.345.194

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 (129) hide show
  1. package/CHANGELOG.md +431 -0
  2. package/assets/bot-workers/setup/worker.yaml +91 -34
  3. package/dist/command-catalog.generated.d.ts +145 -19
  4. package/dist/command-catalog.generated.js +184 -21
  5. package/dist/commands/bot-jobs.d.ts +21 -18
  6. package/dist/commands/bot-jobs.js +127 -39
  7. package/dist/commands/bot.d.ts +4 -3
  8. package/dist/commands/bot.js +265 -140
  9. package/dist/commands/core-jobs-schedule.js +10 -0
  10. package/dist/commands/daemon.js +37 -0
  11. package/dist/commands/files.d.ts +41 -6
  12. package/dist/commands/files.js +108 -27
  13. package/dist/commands/pack-start-bot.d.ts +4 -6
  14. package/dist/commands/pack-start-bot.js +4 -8
  15. package/dist/commands/signals-run.d.ts +46 -0
  16. package/dist/commands/signals-run.js +180 -0
  17. package/dist/commands/signals.d.ts +1 -0
  18. package/dist/commands/signals.js +3 -0
  19. package/dist/commands/sources.d.ts +58 -0
  20. package/dist/commands/sources.js +294 -26
  21. package/dist/commands/staff/reset-test-account.d.ts +10 -0
  22. package/dist/commands/staff/reset-test-account.js +18 -2
  23. package/dist/commands/whoami.d.ts +7 -0
  24. package/dist/commands/whoami.js +35 -2
  25. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
  26. package/dist/hq-cloud-sync-runner/package.json +1 -1
  27. package/dist/lib/bot/api.d.ts +3 -2
  28. package/dist/lib/bot/config.d.ts +2 -0
  29. package/dist/lib/bot/fleet-cron.d.ts +121 -0
  30. package/dist/lib/bot/fleet-cron.js +319 -0
  31. package/dist/lib/bot/fleet-local.d.ts +93 -1
  32. package/dist/lib/bot/fleet-local.js +546 -34
  33. package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
  34. package/dist/lib/bot/fleet-owner-companies.js +30 -0
  35. package/dist/lib/bot/fleet-watch.d.ts +25 -0
  36. package/dist/lib/bot/fleet-watch.js +51 -0
  37. package/dist/lib/bot/index.d.ts +1 -1
  38. package/dist/lib/bot/index.js +1 -1
  39. package/dist/lib/bot/job-run.d.ts +5 -4
  40. package/dist/lib/bot/job-run.js +5 -4
  41. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
  42. package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
  43. package/dist/lib/bot/local-jobs.d.ts +13 -2
  44. package/dist/lib/bot/local-jobs.js +17 -4
  45. package/dist/lib/bot/progress.d.ts +14 -0
  46. package/dist/lib/bot/progress.js +47 -1
  47. package/dist/lib/bot/run.d.ts +55 -2
  48. package/dist/lib/bot/run.js +192 -10
  49. package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
  50. package/dist/lib/bot/setup-tool-offer.js +16 -0
  51. package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
  52. package/dist/lib/bot/signals-engine-flag.js +30 -0
  53. package/dist/lib/daemon/catalog.d.ts +4 -0
  54. package/dist/lib/daemon/catalog.js +29 -9
  55. package/dist/lib/daemon/legacy.d.ts +6 -0
  56. package/dist/lib/daemon/legacy.js +16 -10
  57. package/dist/lib/daemon/run.d.ts +1 -0
  58. package/dist/lib/daemon/run.js +10 -0
  59. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  60. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  61. package/dist/lib/flag-registry.d.ts +13 -1
  62. package/dist/lib/flag-registry.js +46 -0
  63. package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
  64. package/dist/lib/hot-channels/adapters/attio.js +367 -29
  65. package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
  66. package/dist/lib/hot-channels/adapters/gmail.js +59 -2
  67. package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
  68. package/dist/lib/hot-channels/adapters/linear.js +71 -1
  69. package/dist/lib/hot-channels/adapters/meetings.js +4 -1
  70. package/dist/lib/hot-channels/adapters/slack.js +7 -3
  71. package/dist/lib/hot-channels/alert.js +1 -1
  72. package/dist/lib/hot-channels/contract.d.ts +11 -0
  73. package/dist/lib/hot-channels/contract.js +1 -0
  74. package/dist/lib/hot-channels/cursors.js +1 -1
  75. package/dist/lib/hot-channels/landing.d.ts +6 -2
  76. package/dist/lib/hot-channels/landing.js +13 -6
  77. package/dist/lib/hot-channels/ledger.js +2 -0
  78. package/dist/lib/hot-channels/loop.d.ts +6 -0
  79. package/dist/lib/hot-channels/loop.js +19 -4
  80. package/dist/lib/hot-channels/registry.js +9 -1
  81. package/dist/lib/hot-channels/run.d.ts +18 -0
  82. package/dist/lib/hot-channels/run.js +15 -4
  83. package/dist/lib/hot-channels/signals-pass.d.ts +34 -2
  84. package/dist/lib/hot-channels/signals-pass.js +149 -38
  85. package/dist/lib/hot-channels/signals-redo.d.ts +60 -0
  86. package/dist/lib/hot-channels/signals-redo.js +207 -0
  87. package/dist/lib/hot-channels/sync-lock.js +22 -6
  88. package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
  89. package/dist/lib/mesh/live/daemon/install.js +117 -30
  90. package/dist/lib/signals/adapters-catalog.d.ts +77 -0
  91. package/dist/lib/signals/adapters-catalog.js +194 -0
  92. package/dist/lib/signals/engine.d.ts +202 -0
  93. package/dist/lib/signals/engine.js +504 -0
  94. package/dist/lib/signals/failure-alert.d.ts +89 -0
  95. package/dist/lib/signals/failure-alert.js +175 -0
  96. package/dist/lib/signals/local-date.d.ts +3 -0
  97. package/dist/lib/signals/local-date.js +6 -0
  98. package/dist/lib/signals/projection.d.ts +42 -0
  99. package/dist/lib/signals/projection.js +67 -0
  100. package/dist/lib/signals/reply.d.ts +112 -0
  101. package/dist/lib/signals/reply.js +368 -0
  102. package/dist/lib/signals/run-lock.d.ts +36 -0
  103. package/dist/lib/signals/run-lock.js +148 -0
  104. package/dist/lib/signals/run-record.d.ts +24 -0
  105. package/dist/lib/signals/run-record.js +82 -0
  106. package/dist/lib/signals/sanitize.d.ts +105 -0
  107. package/dist/lib/signals/sanitize.js +439 -0
  108. package/dist/lib/signals/settings.d.ts +10 -0
  109. package/dist/lib/signals/settings.js +48 -0
  110. package/dist/lib/signals/steps/adapters.d.ts +14 -0
  111. package/dist/lib/signals/steps/adapters.js +156 -0
  112. package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
  113. package/dist/lib/signals/steps/entity-similarity.js +47 -0
  114. package/dist/lib/signals/steps/extract.d.ts +118 -0
  115. package/dist/lib/signals/steps/extract.js +793 -0
  116. package/dist/lib/signals/steps/garden.d.ts +24 -0
  117. package/dist/lib/signals/steps/garden.js +250 -0
  118. package/dist/lib/signals/steps/types.d.ts +117 -0
  119. package/dist/lib/signals/steps/types.js +4 -0
  120. package/dist/lib/signals/steps/upload.d.ts +43 -0
  121. package/dist/lib/signals/steps/upload.js +324 -0
  122. package/dist/lib/signals/write-guard.d.ts +63 -0
  123. package/dist/lib/signals/write-guard.js +287 -0
  124. package/dist/utils/resident-package-preload.js +16 -0
  125. package/dist/utils/vault-api.d.ts +1 -0
  126. package/dist/utils/vault-api.js +1 -1
  127. package/package.json +9 -9
  128. package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
  129. package/dist/lib/bot/fleet-default-flag.js +0 -127
@@ -23,7 +23,9 @@
23
23
  import { Command } from "commander";
24
24
  import { type RunLedgerAdapterSummary, type RunLedgerEntry, type RunTrigger, type SignalsGeneratedFn } from "../lib/hot-channels/ledger.js";
25
25
  import type { HotChannelRegistry } from "../lib/hot-channels/registry.js";
26
+ import { type AdapterFlagReader } from "../lib/signals/adapters-catalog.js";
26
27
  import { type SelectionLoader } from "../lib/hot-channels/selection.js";
28
+ import { type FleetCronOps } from "../lib/bot/fleet-cron.js";
27
29
  import { type ScheduleReport } from "../lib/hot-channels/schedule.js";
28
30
  import { type OnDemandSyncDeps, type OnDemandSyncReport } from "../lib/hot-channels/run.js";
29
31
  type ListFormat = "table" | "json";
@@ -74,6 +76,13 @@ export interface CompanyHotChannelSyncInput {
74
76
  hqRoot: string;
75
77
  company: string;
76
78
  adapter?: string;
79
+ /** Registered adapter ids allowed to run; see OnDemandSyncInput.onlyAdapters. */
80
+ onlyAdapters?: readonly string[];
81
+ /**
82
+ * Company flag reader for the adapter gate. Defaults to the shared hq-flags
83
+ * evaluator for this company with the signed-in session.
84
+ */
85
+ isFlagOn?: AdapterFlagReader;
77
86
  registry?: HotChannelRegistry;
78
87
  now?: () => Date;
79
88
  /** `scheduled` for the bot's hot-signal-sync job; `on-demand` by default. */
@@ -119,6 +128,53 @@ export declare function runCompanyHotChannelSync(input: CompanyHotChannelSyncInp
119
128
  * line under `companies/{company}/sources/`, and nothing else.
120
129
  */
121
130
  export declare function runSync(options: SyncOptions, overrides?: SyncOverrides): Promise<OnDemandSyncReport>;
131
+ export interface SignalsRedoOptions {
132
+ company?: string;
133
+ since?: string;
134
+ key?: string[];
135
+ channel?: string;
136
+ maxDocs?: number;
137
+ dryRun?: boolean;
138
+ json?: boolean;
139
+ hqRoot: string;
140
+ }
141
+ export interface SignalsRedoOverrides {
142
+ now?: () => Date;
143
+ /** The signed-in person; defaults to the local Cognito session. */
144
+ author?: {
145
+ email: string;
146
+ } | null;
147
+ /** The signals pass; defaults to the local `claude -p` pass with `maxDocs`. */
148
+ createPass?: (maxDocs: number) => SignalsGeneratedFn;
149
+ worklistScript?: string;
150
+ }
151
+ export interface SignalsRedoReport {
152
+ company: string;
153
+ dryRun: boolean;
154
+ /** Keys queued per channel, by how they were queued. */
155
+ queued: {
156
+ pending: Record<string, string[]>;
157
+ worklist: Record<string, string[]>;
158
+ };
159
+ signalsWritten: number;
160
+ /** Keys still waiting in each channel's pending file after the run. */
161
+ pendingLeft: Record<string, number>;
162
+ notices: string[];
163
+ error: string | null;
164
+ exitCode: number;
165
+ }
166
+ /**
167
+ * `hq sources signals`: queue landed documents for the signals pass and run
168
+ * the pass now, under the same per-person lock as `hq sources sync`. The pass
169
+ * keeps its cap (`--max-docs`, default 20); what the cap leaves stays queued
170
+ * and the next run (this command or the daily sync) picks it up.
171
+ */
172
+ export declare function runSignalsRedo(options: SignalsRedoOptions, overrides?: SignalsRedoOverrides): Promise<SignalsRedoReport>;
173
+ /** A usage or setup failure `runSignalsRedo` already printed; the exit code is set. */
174
+ export declare class SignalsRedoExit extends Error {
175
+ readonly exitCode: number;
176
+ constructor(message: string, exitCode: number);
177
+ }
122
178
  export interface ScheduleOptions {
123
179
  company?: string;
124
180
  at?: string;
@@ -131,6 +187,8 @@ export interface ScheduleOverrides {
131
187
  botsRoot?: string;
132
188
  /** Whose ledger to read; defaults to the signed-in person. */
133
189
  email?: string;
190
+ /** The HQ Fleet Runtime schedule of one bot; defaults to the bot's own runtime. */
191
+ fleetOps?: (agentUid: string) => FleetCronOps;
134
192
  }
135
193
  export declare class NoHotSignalSyncJobError extends Error {
136
194
  readonly company: string;
@@ -23,6 +23,7 @@
23
23
  import { randomUUID } from "node:crypto";
24
24
  import * as fs from "node:fs";
25
25
  import * as path from "node:path";
26
+ import { InvalidArgumentError } from "commander";
26
27
  import chalk from "chalk";
27
28
  import { SOURCE_CHANNELS, assertSourceChannel, resolveEntity, listAvailableEntities, listSources, getSource, VaultClient, } from "@indigoai-us/hq-cloud";
28
29
  import { ensureCognitoToken, ensureCognitoIdToken, buildVaultConfig, DEFAULT_COGNITO, DEFAULT_HQ_ROOT, } from "../utils/cognito-session.js";
@@ -34,16 +35,20 @@ import { resolveUploadAuthor } from "../utils/upload-author.js";
34
35
  import { createSignalsAlertChannelPoster, raiseSignalsFailureAlerts } from "../lib/hot-channels/alert.js";
35
36
  import { audienceKey } from "../lib/hot-channels/audience.js";
36
37
  import { appendRunLedgerEntry, ledgerDay, readRunLedger, summarizeRunLedger, } from "../lib/hot-channels/ledger.js";
37
- import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
38
+ import { createClaudeSignalsPass, DEFAULT_MAX_SIGNAL_DOCS, readPendingSignals, SignalsPassError, } from "../lib/hot-channels/signals-pass.js";
39
+ import { backlogRunResult, parseSince, queueSignalsRedo, selectSignalsRedo, SignalsRedoUsageError, } from "../lib/hot-channels/signals-redo.js";
38
40
  import { assertSafeSegment } from "../lib/hot-channels/landing.js";
39
41
  import { acquireSyncLock, SYNC_LOCK_BUSY_REASON } from "../lib/hot-channels/sync-lock.js";
40
42
  import { createBuiltinHotChannelRegistry } from "../lib/hot-channels/adapters/index.js";
43
+ import { companyAdapterFlagReader, gateAdapters, readSignalsAdaptersCatalog, } from "../lib/signals/adapters-catalog.js";
41
44
  import { createSelectionLoader } from "../lib/hot-channels/selection.js";
42
45
  import { createPersonVaultReader } from "../lib/hot-channels/person-vault-reader.js";
43
46
  import { S3Client } from "@aws-sdk/client-s3";
44
47
  import { resolveCanonicalPersonUid } from "./cloud.js";
45
48
  import { botsRoot as defaultBotsRoot } from "../lib/bot/paths.js";
46
49
  import { ensureHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
50
+ import { companyOfHotSignalSyncJob, fleetCronOps } from "../lib/bot/fleet-cron.js";
51
+ import { HQ_FLEET_RUNTIME_ENGINE_ID, readBotConfig } from "../lib/bot/config.js";
47
52
  import { findHotSignalSyncJobs, lastRunsPerAdapter, parseDailyAt, renderScheduleReport, rescheduleDaily, toJobView, } from "../lib/hot-channels/schedule.js";
48
53
  import { listActiveMembers } from "./members.js";
49
54
  import { runOnDemandSync, } from "../lib/hot-channels/run.js";
@@ -485,32 +490,78 @@ async function skippedReport(input, vaultRoot, personKey, personUid, now) {
485
490
  runId: line.runId,
486
491
  signInRequired: false,
487
492
  skipped: true,
493
+ personKey,
488
494
  outcomes: [],
489
495
  entries: [line],
490
496
  notices: [`Another hot-channel sync for ${input.company} is already running, so this one was skipped.`],
491
497
  exitCode: 0,
492
498
  };
493
499
  }
500
+ /**
501
+ * One `gated` outcome per adapter the local gate turned off (yaml or company
502
+ * flag). With `--adapter`, only that adapter's. Nothing is fetched for them.
503
+ */
504
+ function gatedAdapterOutcomes(registry, gating, wanted) {
505
+ const want = wanted?.toLowerCase();
506
+ const out = [];
507
+ for (const adapter of registry.list()) {
508
+ if (!(adapter.id in gating.skips))
509
+ continue;
510
+ const provider = adapter.tokenScope.provider || adapter.id;
511
+ if (want !== undefined && adapter.id !== want && provider.toLowerCase() !== want)
512
+ continue;
513
+ const flag = gating.offFlags[adapter.id];
514
+ out.push({
515
+ adapter: adapter.id,
516
+ provider,
517
+ connectionId: null,
518
+ appName: null,
519
+ status: "gated",
520
+ landed: 0,
521
+ duplicates: 0,
522
+ message: flag
523
+ ? `${adapter.id} is not enabled for this company (flag ${flag} is off)`
524
+ : `${adapter.id} is turned off in core/settings/signals-adapters.yaml`,
525
+ });
526
+ }
527
+ return out;
528
+ }
494
529
  async function runLockedCompanySync(input, vaultRoot, personKey, author, session, lockHeld) {
495
530
  const deps = gatewaySyncDeps(session.token, session.companyUid);
496
- // A fresh loader per run: selections are cached for this run only.
497
- const selections = input.createSelections ? input.createSelections() : defaultSelections();
498
- const report = await runOnDemandSync({
499
- ...(selections ? { selections } : {}),
500
- deps,
501
- registry: input.registry ??
502
- createBuiltinHotChannelRegistry({ vaultRoot, identities: await meetingIdentities(input, session, author.email) }),
503
- vaultRoot,
504
- personUid: author.userSub,
505
- personKey,
506
- ...(input.adapter !== undefined ? { adapter: input.adapter } : {}),
507
- ...(input.now ? { now: input.now } : {}),
508
- ...(input.trigger ? { trigger: input.trigger } : {}),
509
- ...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
510
- ...(input.firstRun ? { firstRun: true } : {}),
511
- ...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
512
- beforeWrite: lockHeld,
513
- });
531
+ const registry = input.registry ??
532
+ createBuiltinHotChannelRegistry({ vaultRoot, identities: await meetingIdentities(input, session, author.email) });
533
+ // The adapter gate for every caller: signals-adapters.yaml plus the company
534
+ // flags, read through hq-flags.
535
+ const gating = await gateAdapters(registry.list().map((a) => a.id), readSignalsAdaptersCatalog(input.hqRoot), input.isFlagOn ??
536
+ companyAdapterFlagReader(session.companyUid, session.token, { hqRoot: input.hqRoot, company: input.company }));
537
+ const gated = gatedAdapterOutcomes(registry, gating, input.adapter);
538
+ const onlyAdapters = (input.onlyAdapters ?? gating.enabled).filter((id) => gating.enabled.includes(id));
539
+ let report;
540
+ if (input.adapter !== undefined && gated.length > 0) {
541
+ // `--adapter` named an adapter the gate turned off: there is nothing to run.
542
+ report = { runId: null, signInRequired: false, personKey, outcomes: [], entries: [], notices: [], exitCode: 0 };
543
+ }
544
+ else {
545
+ // A fresh loader per run: selections are cached for this run only.
546
+ const selections = input.createSelections ? input.createSelections() : defaultSelections();
547
+ report = await runOnDemandSync({
548
+ ...(selections ? { selections } : {}),
549
+ deps,
550
+ registry,
551
+ vaultRoot,
552
+ personUid: author.userSub,
553
+ personKey,
554
+ ...(input.adapter !== undefined ? { adapter: input.adapter } : {}),
555
+ ...(input.onlyAdapters !== undefined || Object.keys(gating.skips).length > 0 ? { onlyAdapters } : {}),
556
+ ...(input.now ? { now: input.now } : {}),
557
+ ...(input.trigger ? { trigger: input.trigger } : {}),
558
+ ...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
559
+ ...(input.firstRun ? { firstRun: true } : {}),
560
+ ...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
561
+ beforeWrite: lockHeld,
562
+ });
563
+ }
564
+ report.outcomes.push(...gated);
514
565
  if (input.signalsAlert) {
515
566
  const postCompanyChannel = input.signalsAlert.postCompanyChannel ??
516
567
  createSignalsAlertChannelPoster({ hqRoot: input.hqRoot, company: input.company, deps });
@@ -593,6 +644,188 @@ function renderSyncTable(company, report) {
593
644
  }
594
645
  return lines.join("\n");
595
646
  }
647
+ /**
648
+ * `hq sources signals`: queue landed documents for the signals pass and run
649
+ * the pass now, under the same per-person lock as `hq sources sync`. The pass
650
+ * keeps its cap (`--max-docs`, default 20); what the cap leaves stays queued
651
+ * and the next run (this command or the daily sync) picks it up.
652
+ */
653
+ export async function runSignalsRedo(options, overrides = {}) {
654
+ const company = options.company ?? readActiveCompanySlug(options.hqRoot);
655
+ const fail = (message, exitCode) => {
656
+ process.stderr.write(chalk.red(`✗ ${message}\n`));
657
+ process.exitCode = exitCode;
658
+ throw new SignalsRedoExit(message, exitCode);
659
+ };
660
+ if (!company)
661
+ return fail("No company specified. Pass --company <slug> or set activeCompany in .hq/config.json.", 2);
662
+ assertSafeSegment(company, "company");
663
+ if (options.since === undefined && (options.key ?? []).length === 0)
664
+ return fail("Pass --since <date> or --key <sources/channel/id.md>.", 2);
665
+ const vaultRoot = path.join(options.hqRoot, "companies", company);
666
+ if (!fs.existsSync(vaultRoot))
667
+ return fail(`No company directory: companies/${company}`, 2);
668
+ const maxDocs = options.maxDocs ?? DEFAULT_MAX_SIGNAL_DOCS;
669
+ let selection;
670
+ try {
671
+ selection = await selectSignalsRedo({
672
+ vaultRoot,
673
+ ...(options.key ? { keys: options.key } : {}),
674
+ ...(options.since !== undefined ? { since: parseSince(options.since) } : {}),
675
+ ...(options.channel !== undefined ? { channel: options.channel } : {}),
676
+ });
677
+ }
678
+ catch (err) {
679
+ if (err instanceof SignalsRedoUsageError)
680
+ return fail(err.message, 2);
681
+ throw err;
682
+ }
683
+ const report = {
684
+ company,
685
+ dryRun: options.dryRun === true,
686
+ queued: { pending: Object.fromEntries(selection.pending), worklist: Object.fromEntries(selection.worklist) },
687
+ signalsWritten: 0,
688
+ pendingLeft: {},
689
+ notices: [...selection.notices],
690
+ error: null,
691
+ exitCode: 0,
692
+ };
693
+ const channels = [...new Set([...selection.pending.keys(), ...selection.worklist.keys()])];
694
+ if (!report.dryRun && channels.length > 0) {
695
+ const author = overrides.author !== undefined ? overrides.author : resolveUploadAuthor();
696
+ if (!author)
697
+ return fail(`${SIGN_IN_PROMPT} (the run takes your hot-channel sync lock)`, 1);
698
+ const lock = await acquireSyncLock(vaultRoot, audienceKey([author.email]), overrides.now ? { now: overrides.now } : {});
699
+ if (!lock)
700
+ return fail(`Another hot-channel sync for ${company} is running. Try again when it finishes.`, 1);
701
+ try {
702
+ await queueSignalsRedo({
703
+ hqRoot: options.hqRoot,
704
+ company,
705
+ selection,
706
+ beforeWrite: lock.assertHeld,
707
+ ...(overrides.worklistScript ? { worklistScript: overrides.worklistScript } : {}),
708
+ });
709
+ const pass = overrides.createPass
710
+ ? overrides.createPass(maxDocs)
711
+ : createClaudeSignalsPass({ hqRoot: options.hqRoot, company, maxDocs });
712
+ for (const channel of channels) {
713
+ report.signalsWritten += await pass(backlogRunResult(channel), { beforeWrite: lock.assertHeld });
714
+ }
715
+ }
716
+ catch (err) {
717
+ report.error = err instanceof SignalsPassError ? err.code : err instanceof Error ? err.message : String(err);
718
+ report.exitCode = 1;
719
+ }
720
+ finally {
721
+ await lock.release();
722
+ }
723
+ }
724
+ for (const channel of selection.pending.keys()) {
725
+ report.pendingLeft[channel] = (await readPendingSignals(vaultRoot, channel)).length;
726
+ }
727
+ if (options.json) {
728
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
729
+ }
730
+ else {
731
+ process.stdout.write(`${renderSignalsRedo(report)}\n`);
732
+ }
733
+ if (report.exitCode !== 0)
734
+ process.exitCode = report.exitCode;
735
+ return report;
736
+ }
737
+ /** A usage or setup failure `runSignalsRedo` already printed; the exit code is set. */
738
+ export class SignalsRedoExit extends Error {
739
+ exitCode;
740
+ constructor(message, exitCode) {
741
+ super(message);
742
+ this.name = "SignalsRedoExit";
743
+ this.exitCode = exitCode;
744
+ }
745
+ }
746
+ function renderSignalsRedo(report) {
747
+ const lines = [`${report.dryRun ? "Would queue" : "Queued"} for signals in ${report.company}:`];
748
+ const groups = [
749
+ ...Object.entries(report.queued.pending).map(([channel, keys]) => ({ channel, keys, how: "pending" })),
750
+ ...Object.entries(report.queued.worklist).map(([channel, keys]) => ({ channel, keys, how: "worklist --redo" })),
751
+ ];
752
+ if (groups.length === 0)
753
+ lines.push(" nothing (no matching landed document without signals)");
754
+ for (const group of groups) {
755
+ lines.push(` sources/${group.channel}: ${group.keys.length} (${group.how})`);
756
+ for (const key of group.keys)
757
+ lines.push(` ${key}`);
758
+ }
759
+ lines.push(...report.notices);
760
+ if (!report.dryRun && groups.length > 0) {
761
+ lines.push("", `${report.signalsWritten} signal(s) written.`);
762
+ for (const [channel, left] of Object.entries(report.pendingLeft)) {
763
+ if (left > 0)
764
+ lines.push(`${left} document(s) still queued in sources/${channel}; the next run picks them up.`);
765
+ }
766
+ if (report.error)
767
+ lines.push(chalk.red(`The signals pass stopped: ${report.error}. Queued documents stay queued.`));
768
+ }
769
+ return lines.join("\n");
770
+ }
771
+ function collectKey(value, previous = []) {
772
+ return [...previous, value];
773
+ }
774
+ function parseMaxDocs(value) {
775
+ const n = Number(value);
776
+ if (!Number.isInteger(n) || n <= 0)
777
+ throw new InvalidArgumentError("must be a positive integer");
778
+ return n;
779
+ }
780
+ function fleetBotsIn(root) {
781
+ let names;
782
+ try {
783
+ names = fs.readdirSync(root).sort();
784
+ }
785
+ catch {
786
+ return [];
787
+ }
788
+ const bots = [];
789
+ for (const botName of names) {
790
+ const config = readBotConfig(path.join(root, botName));
791
+ if (config && config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID)
792
+ bots.push({ botName, agentUid: config.agentUid });
793
+ }
794
+ return bots;
795
+ }
796
+ function findFleetHotSignalSyncJob(root, company, opsFor) {
797
+ for (const bot of fleetBotsIn(root)) {
798
+ let ops;
799
+ let jobs;
800
+ try {
801
+ ops = opsFor(bot.agentUid);
802
+ jobs = ops.list();
803
+ }
804
+ catch {
805
+ continue;
806
+ }
807
+ const job = jobs.find((candidate) => companyOfHotSignalSyncJob(candidate) === company);
808
+ if (job)
809
+ return { botName: bot.botName, ops, job };
810
+ }
811
+ return null;
812
+ }
813
+ function fleetDailyAt(expr) {
814
+ const match = /^(\d{1,2}) (\d{1,2}) \* \* \*$/.exec((expr ?? "").trim());
815
+ return match ? `${match[2].padStart(2, "0")}:${match[1].padStart(2, "0")}` : null;
816
+ }
817
+ function fleetJobView(found) {
818
+ const expr = found.job.schedule?.expr ?? "";
819
+ return {
820
+ botName: found.botName,
821
+ jobId: found.job.id,
822
+ cron: expr,
823
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
824
+ dailyAt: fleetDailyAt(expr),
825
+ nextRunAt: found.job.next_run_at ?? null,
826
+ lastRunAt: found.job.last_run_at ?? null,
827
+ };
828
+ }
596
829
  export class NoHotSignalSyncJobError extends Error {
597
830
  company;
598
831
  constructor(company) {
@@ -618,7 +851,22 @@ export async function runSchedule(options, overrides = {}) {
618
851
  // Validate before touching any job file.
619
852
  if (options.at !== undefined)
620
853
  parseDailyAt(options.at);
621
- let found = findHotSignalSyncJobs(overrides.botsRoot ?? defaultBotsRoot(), company);
854
+ const botsRootDir = overrides.botsRoot ?? defaultBotsRoot();
855
+ const fleetFound = findFleetHotSignalSyncJob(botsRootDir, company, overrides.fleetOps ?? fleetCronOps);
856
+ if (fleetFound) {
857
+ if (options.at !== undefined) {
858
+ const { hour, minute } = parseDailyAt(options.at);
859
+ const updated = fleetFound.ops.reschedule(fleetFound.job.id, `${minute} ${hour} * * *`);
860
+ if (!updated)
861
+ throw new NoHotSignalSyncJobError(company);
862
+ fleetFound.job = updated;
863
+ }
864
+ return finishScheduleReport(options, overrides, company, fleetJobView(fleetFound), [], now);
865
+ }
866
+ if (options.at !== undefined && fleetBotsIn(botsRootDir).some((bot) => bot.botName === "setup")) {
867
+ throw new Error(`The setup bot runs on the HQ Fleet Runtime and has no hot-signal-sync job for ${company} yet. Start it once with \`hq bot start setup\` so it adds the job, then set the time again.`);
868
+ }
869
+ let found = findHotSignalSyncJobs(botsRootDir, company);
622
870
  if (options.at !== undefined) {
623
871
  if (found.length === 0) {
624
872
  // Setting a time turns the daily sync on: register it on the personal setup bot.
@@ -638,20 +886,19 @@ export async function runSchedule(options, overrides = {}) {
638
886
  throw new NoHotSignalSyncJobError(company);
639
887
  found = rescheduleDaily(found, options.at, now);
640
888
  }
889
+ return finishScheduleReport(options, overrides, company, found[0] ? toJobView(found[0], now) : null, found.slice(1).map((f) => f.botName), now);
890
+ }
891
+ async function finishScheduleReport(options, overrides, company, job, extraBots, now) {
641
892
  const email = overrides.email ?? resolveUploadAuthor()?.email;
642
893
  const lastRuns = email
643
894
  ? await lastRunsPerAdapter(path.join(options.hqRoot, "companies", company), audienceKey([email]), now)
644
895
  : [];
645
- const report = {
646
- company,
647
- job: found[0] ? toJobView(found[0], now) : null,
648
- lastRuns,
649
- };
896
+ const report = { company, job, lastRuns };
650
897
  if (options.json) {
651
898
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
652
899
  }
653
900
  else {
654
- process.stdout.write(renderScheduleReport(report, found.slice(1).map((f) => f.botName)) + "\n");
901
+ process.stdout.write(renderScheduleReport(report, extraBots) + "\n");
655
902
  if (!email)
656
903
  process.stdout.write("Sign in (hq login) to see recent runs.\n");
657
904
  }
@@ -730,6 +977,27 @@ export function registerSourcesCommand(program) {
730
977
  .action(withErrorHandling(async (options) => {
731
978
  await runSchedule(options);
732
979
  }));
980
+ sources
981
+ .command("signals")
982
+ .description("Process landed sources for signals again: queue them for the signals pass and run it now (the pass's document cap still applies)")
983
+ .option("--company <slug>", "Company slug (defaults to .hq/config.json activeCompany)")
984
+ .option("--since <date>", "Landed documents from this date on (YYYY-MM-DD is local midnight) that have no signals yet")
985
+ .option("--key <key>", "A landed document to process again, signals or not (sources/<channel>/<id>.md; repeatable)", collectKey)
986
+ .option("--channel <channel>", "Limit --since to one channel")
987
+ .option("--max-docs <n>", `Documents sent to claude in this run (default ${DEFAULT_MAX_SIGNAL_DOCS})`, parseMaxDocs)
988
+ .option("--dry-run", "Print what would be queued; queue nothing and call no model")
989
+ .option("--json", "Print the report as JSON")
990
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
991
+ .action(withErrorHandling(async (options) => {
992
+ try {
993
+ await runSignalsRedo(options);
994
+ }
995
+ catch (err) {
996
+ // Usage and setup failures are already printed with their exit code.
997
+ if (!(err instanceof SignalsRedoExit))
998
+ throw err;
999
+ }
1000
+ }));
733
1001
  sources
734
1002
  .command("sync")
735
1003
  .description("Run every hot channel now: each adapter once per connection you can use")
@@ -24,6 +24,12 @@ interface PlanCompany {
24
24
  slug: string;
25
25
  memberCount: number;
26
26
  }
27
+ interface PlanCompanyHandoff {
28
+ companyUid: string;
29
+ companySlug: string;
30
+ toPersonUid: string;
31
+ toEmail: string;
32
+ }
27
33
  interface Plan {
28
34
  target: {
29
35
  input: string;
@@ -32,6 +38,8 @@ interface Plan {
32
38
  personUid: string | null;
33
39
  cognitoUsers: PlanCognitoUser[];
34
40
  ownedCompanies: PlanCompany[];
41
+ /** Present only when the request opted in to the handoff. */
42
+ sharedCompanyHandoffs?: PlanCompanyHandoff[];
35
43
  personalVault: {
36
44
  bucketName: string | null;
37
45
  kmsKeyAlias: string | null;
@@ -89,6 +97,8 @@ export interface RunResetTestAccountParams {
89
97
  execute: boolean;
90
98
  /** Skip the typed-confirmation prompt on --execute (for scripts). */
91
99
  yes?: boolean;
100
+ /** Keep a company with real members and hand it to one, instead of refusing. */
101
+ handoffSharedCompanies?: boolean;
92
102
  }
93
103
  export interface RunResetTestAccountDeps {
94
104
  prompt?: ConfirmFn;
@@ -76,6 +76,13 @@ export function renderPlan(plan) {
76
76
  }
77
77
  }
78
78
  lines.push("");
79
+ if (plan.sharedCompanyHandoffs && plan.sharedCompanyHandoffs.length > 0) {
80
+ lines.push(chalk.bold(" Companies kept and handed over"));
81
+ for (const h of plan.sharedCompanyHandoffs) {
82
+ lines.push(` handoff: ${h.companySlug} to ${h.toEmail}`);
83
+ }
84
+ lines.push("");
85
+ }
79
86
  lines.push(chalk.bold(" Personal vault"));
80
87
  lines.push(` bucket: ${plan.personalVault.bucketName ?? chalk.dim("(none)")}`);
81
88
  lines.push(` kms alias: ${plan.personalVault.kmsKeyAlias ?? chalk.dim("(none)")}`);
@@ -166,13 +173,17 @@ export async function runResetTestAccount(params, deps = {}) {
166
173
  throw new Error("reset-test-account requires a super-admin (staff) HQ session. " +
167
174
  "Sign in with an @getindigo.ai account and retry.");
168
175
  }
176
+ // Absent unless asked for, so the server's refusal stays the default.
177
+ const handoff = params.handoffSharedCompanies === true
178
+ ? { handoffSharedCompanies: true }
179
+ : {};
169
180
  // Dry-run first. The AC calls out "Without --execute, runs dry-run and
170
181
  // prints the plan" — so a bare invocation always ends here.
171
182
  const dryRunRes = await vaultApiFetch({
172
183
  token,
173
184
  path: "/v1/admin/test-account/reset",
174
185
  method: "POST",
175
- body: { email, dryRun: true },
186
+ body: { email, dryRun: true, ...handoff },
176
187
  });
177
188
  const dryRunBody = await readBody(dryRunRes);
178
189
  if (!dryRunRes.ok) {
@@ -221,7 +232,7 @@ export async function runResetTestAccount(params, deps = {}) {
221
232
  token,
222
233
  path: "/v1/admin/test-account/reset",
223
234
  method: "POST",
224
- body: { email, dryRun: false, confirmEmail },
235
+ body: { email, dryRun: false, confirmEmail, ...handoff },
225
236
  });
226
237
  const executeBody = await readBody(executeRes);
227
238
  // The handler answers 200 (all-ok) and 207 (best-effort partial). Anything
@@ -234,6 +245,9 @@ export async function runResetTestAccount(params, deps = {}) {
234
245
  throw new Error("server did not return an execute result");
235
246
  }
236
247
  console.log(renderExecuteSummary(result));
248
+ for (const h of plan.sharedCompanyHandoffs ?? []) {
249
+ console.log(` handoff: ${h.companySlug} to ${h.toEmail}`);
250
+ }
237
251
  if (!result.ok) {
238
252
  // Non-zero exit so a scripted caller can react. The summary line above
239
253
  // already shows which step failed.
@@ -253,12 +267,14 @@ export function registerResetTestAccountCommand(staff, deps = {}) {
253
267
  .argument("<email>", "Designated test-account email (must be in the registry)")
254
268
  .option("--execute", "Actually delete artifacts. Prompts for a typed-email confirmation.")
255
269
  .option("-y, --yes", "Skip the typed-email confirmation prompt (still sends confirmEmail).")
270
+ .option("--handoff-shared-companies", "Keep a company with real members and hand it to one, instead of refusing the reset.")
256
271
  .action(async (email, opts) => {
257
272
  try {
258
273
  await runResetTestAccount({
259
274
  email,
260
275
  execute: opts.execute === true,
261
276
  yes: opts.yes === true,
277
+ handoffSharedCompanies: opts.handoffSharedCompanies === true,
262
278
  }, deps);
263
279
  }
264
280
  catch (err) {
@@ -21,6 +21,13 @@ export interface WhoamiDisplayIdentity {
21
21
  * email with the token subject of the delegating machine.
22
22
  */
23
23
  export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
24
+ /**
25
+ * Bound on the person-uid lookup, so `--json` callers with short timeouts still
26
+ * get an answer. Kept at 1s: with every endpoint stalled, a whole `whoami
27
+ * --json` must still finish inside the foreground network budget plus startup
28
+ * (foreground-network-budget.test.ts); 3s overran it.
29
+ */
30
+ export declare const PERSON_UID_LOOKUP_TIMEOUT_MS = 1000;
24
31
  export declare function renderWhoamiPlanBlock(status: PlanLimitStatus, opts?: {
25
32
  inviteTeammateEnabled?: boolean;
26
33
  }): string | null;
@@ -8,6 +8,7 @@ import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource,
8
8
  import { colorizePlanLimitNotice, fetchPlanLimitStatus, renderPlanLine, renderPlanLimitNotice, } from "../lib/billing/plan-lock.js";
9
9
  import { resolveCompanyFlagGate, } from "../lib/flag-registry.js";
10
10
  import { queuePlanLimitInviteTeammateExposure } from "../utils/cli-telemetry.js";
11
+ import { resolveCallerPersonUid } from "../utils/vault-api.js";
11
12
  export const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
12
13
  /**
13
14
  * Resolves the person represented by an ID token without pairing a delegated
@@ -44,6 +45,36 @@ function peekIdToken(idToken) {
44
45
  : undefined,
45
46
  };
46
47
  }
48
+ /**
49
+ * Bound on the person-uid lookup, so `--json` callers with short timeouts still
50
+ * get an answer. Kept at 1s: with every endpoint stalled, a whole `whoami
51
+ * --json` must still finish inside the foreground network budget plus startup
52
+ * (foreground-network-budget.test.ts); 3s overran it.
53
+ */
54
+ export const PERSON_UID_LOOKUP_TIMEOUT_MS = 1_000;
55
+ /**
56
+ * The signed-in person's canonical `prs_*` uid for `whoami --json`. Person ID
57
+ * tokens carry no `custom:entityType`/`custom:entityUid` claims, so the token
58
+ * alone almost never has it; the vault's person-entity lookup does (the same
59
+ * rule `hq secrets --personal` uses). Best-effort: no session, a timeout or a
60
+ * network failure resolve to null and the rest of the identity still prints.
61
+ */
62
+ async function readPersonUid(identity) {
63
+ if (identity.entityType === 'person' && identity.entityUid)
64
+ return identity.entityUid;
65
+ try {
66
+ const token = await ensureCognitoToken({ interactive: false });
67
+ if (!token)
68
+ return null;
69
+ return await resolveCallerPersonUid(token, undefined, {
70
+ command: 'hq whoami',
71
+ signal: AbortSignal.timeout(PERSON_UID_LOOKUP_TIMEOUT_MS),
72
+ });
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
47
78
  /**
48
79
  * starter-plan-hard-limits / US-011 — read the workspace plan-limit nag for
49
80
  * `--company <slug>` (the bind notice).
@@ -148,9 +179,11 @@ export function registerWhoamiCommand(program) {
148
179
  : boundLegacyAgent;
149
180
  // planLimits is additive: absent-or-null means UNKNOWN.
150
181
  const planLimits = await readPlanLimits(opts.company);
182
+ const authenticated = machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0));
183
+ const personUid = !machine && authenticated ? await readPersonUid(identity) : null;
151
184
  console.log(JSON.stringify({
152
185
  schemaVersion: 1,
153
- authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
186
+ authenticated,
154
187
  tokenSource: machine ? 'machine' : 'person',
155
188
  email: machine ? null : displayedIdentity.email ?? null,
156
189
  // Cognito subject of the person shown in `email` (null for machine
@@ -162,7 +195,7 @@ export function registerWhoamiCommand(program) {
162
195
  ? identity.delegatedEmail : null,
163
196
  delegatedSub: identity.delegatedEmail !== undefined && identity.delegatedSub !== undefined
164
197
  ? identity.delegatedSub : null,
165
- personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
198
+ personUid,
166
199
  agentUid,
167
200
  username: machineCreds?.username ?? null,
168
201
  planLimits,