@indigoai-us/hq-cli 5.345.193 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.194] — 2026-11-03
6
+ - Meeting and other hot-channel documents that land over the signals pass's
7
+ 20-document limit now get signals on a later run. Before this fix, a large
8
+ refresh (for example 40 meetings at once) left everything past the first 20
9
+ without signals for good. Each run now works through the channel's backlog,
10
+ newest first, up to 20 documents, even when the run landed nothing new. For
11
+ a channel with an ontology worklist the backlog is the worklist; for a hot
12
+ channel without one (such as meeting pointers) the pass keeps its own queue
13
+ in `sources/_signals-pending/`. When there is nothing to process, claude is
14
+ not called.
15
+ - New `hq sources signals --company <co> --since <date>` processes landed
16
+ documents from that date on that never got signals, and `--key
17
+ sources/<channel>/<id>.md` (repeatable) processes one again. The 20-document
18
+ limit still applies (`--max-docs` changes it); what it leaves stays queued
19
+ for the next run. `--dry-run` lists what would be queued.
20
+ - `hq signals run --company <co> --redo sources/<channel>/<file>.md` (can be
21
+ repeated) processes an ontology source item again. It needs the ontology
22
+ worklist script with `--redo` (hq-core-staging #1781).
23
+ - The six native hook packages (one per platform, `@indigoai-us/hq-hook-win32-x64`
24
+ and siblings) now publish to npm through OIDC trusted publishing without a
25
+ provenance statement. The v5.345.193 release failed every hook package job
26
+ because npm rejects provenance from a private source repository (E422), and
27
+ hq-cli is private. The main `@indigoai-us/hq-cli` package already publishes
28
+ this way.
29
+ - `hq whoami --json` now reports the signed-in person's `personUid` (`prs_*`).
30
+ Person sign-in tokens carry no entity claims, so the field was always null
31
+ for people, and the Daily Brief read only the email-keyed signals folder and
32
+ missed each company's signals for that person. whoami now asks the vault for
33
+ the caller's person entity, the same lookup `hq secrets --personal` uses,
34
+ bounded to 1 second. When the lookup fails or times out, `personUid` stays
35
+ null and the rest of the output is unchanged. Machine (agent) identities and
36
+ expired sessions make no lookup.
37
+
5
38
  ## [5.345.193] — 2026-11-03
6
39
  - HQ's setup assistant answers the desktop app's end-of-setup handoff as the
7
40
  assistant it is: who it is and what it can help with (questions, finding
@@ -5335,6 +5335,51 @@ export declare const COMMAND_CATALOG: readonly [{
5335
5335
  readonly defaultValue: "<runtime-path>";
5336
5336
  }];
5337
5337
  readonly subcommands: readonly [];
5338
+ }, {
5339
+ readonly name: "signals";
5340
+ readonly description: "Process landed sources for signals again: queue them for the signals pass and run it now (the pass's document cap still applies)";
5341
+ readonly aliases: readonly [];
5342
+ readonly hidden: false;
5343
+ readonly helpOption: true;
5344
+ readonly helpCommand: false;
5345
+ readonly usage: "[options]";
5346
+ readonly helpText: readonly [];
5347
+ readonly arguments: readonly [];
5348
+ readonly options: readonly [{
5349
+ readonly flags: "--company <slug>";
5350
+ readonly description: "Company slug (defaults to .hq/config.json activeCompany)";
5351
+ readonly hidden: false;
5352
+ }, {
5353
+ readonly flags: "--since <date>";
5354
+ readonly description: "Landed documents from this date on (YYYY-MM-DD is local midnight) that have no signals yet";
5355
+ readonly hidden: false;
5356
+ }, {
5357
+ readonly flags: "--key <key>";
5358
+ readonly description: "A landed document to process again, signals or not (sources/<channel>/<id>.md; repeatable)";
5359
+ readonly hidden: false;
5360
+ }, {
5361
+ readonly flags: "--channel <channel>";
5362
+ readonly description: "Limit --since to one channel";
5363
+ readonly hidden: false;
5364
+ }, {
5365
+ readonly flags: "--max-docs <n>";
5366
+ readonly description: "Documents sent to claude in this run (default 20)";
5367
+ readonly hidden: false;
5368
+ }, {
5369
+ readonly flags: "--dry-run";
5370
+ readonly description: "Print what would be queued; queue nothing and call no model";
5371
+ readonly hidden: false;
5372
+ }, {
5373
+ readonly flags: "--json";
5374
+ readonly description: "Print the report as JSON";
5375
+ readonly hidden: false;
5376
+ }, {
5377
+ readonly flags: "--hq-root <path>";
5378
+ readonly description: "Local HQ tree root (default: <runtime-path>)";
5379
+ readonly hidden: false;
5380
+ readonly defaultValue: "<runtime-path>";
5381
+ }];
5382
+ readonly subcommands: readonly [];
5338
5383
  }, {
5339
5384
  readonly name: "sync";
5340
5385
  readonly description: "Run every hot channel now: each adapter once per connection you can use";
@@ -5540,6 +5585,10 @@ export declare const COMMAND_CATALOG: readonly [{
5540
5585
  readonly flags: "--report-found";
5541
5586
  readonly description: "Print the owner's run reply only when the run found new signals; print nothing otherwise";
5542
5587
  readonly hidden: false;
5588
+ }, {
5589
+ readonly flags: "--redo <file>";
5590
+ readonly description: "Process a source item again (sources/<channel>/<file>.md; repeatable; needs --company): the extract step re-queues it in the ontology worklist before listing";
5591
+ readonly hidden: false;
5543
5592
  }, {
5544
5593
  readonly flags: "--hq-root <path>";
5545
5594
  readonly description: "HQ root (default: <runtime-path>)";
@@ -6471,6 +6471,61 @@ export const COMMAND_CATALOG = [
6471
6471
  ],
6472
6472
  "subcommands": []
6473
6473
  },
6474
+ {
6475
+ "name": "signals",
6476
+ "description": "Process landed sources for signals again: queue them for the signals pass and run it now (the pass's document cap still applies)",
6477
+ "aliases": [],
6478
+ "hidden": false,
6479
+ "helpOption": true,
6480
+ "helpCommand": false,
6481
+ "usage": "[options]",
6482
+ "helpText": [],
6483
+ "arguments": [],
6484
+ "options": [
6485
+ {
6486
+ "flags": "--company <slug>",
6487
+ "description": "Company slug (defaults to .hq/config.json activeCompany)",
6488
+ "hidden": false
6489
+ },
6490
+ {
6491
+ "flags": "--since <date>",
6492
+ "description": "Landed documents from this date on (YYYY-MM-DD is local midnight) that have no signals yet",
6493
+ "hidden": false
6494
+ },
6495
+ {
6496
+ "flags": "--key <key>",
6497
+ "description": "A landed document to process again, signals or not (sources/<channel>/<id>.md; repeatable)",
6498
+ "hidden": false
6499
+ },
6500
+ {
6501
+ "flags": "--channel <channel>",
6502
+ "description": "Limit --since to one channel",
6503
+ "hidden": false
6504
+ },
6505
+ {
6506
+ "flags": "--max-docs <n>",
6507
+ "description": "Documents sent to claude in this run (default 20)",
6508
+ "hidden": false
6509
+ },
6510
+ {
6511
+ "flags": "--dry-run",
6512
+ "description": "Print what would be queued; queue nothing and call no model",
6513
+ "hidden": false
6514
+ },
6515
+ {
6516
+ "flags": "--json",
6517
+ "description": "Print the report as JSON",
6518
+ "hidden": false
6519
+ },
6520
+ {
6521
+ "flags": "--hq-root <path>",
6522
+ "description": "Local HQ tree root (default: <runtime-path>)",
6523
+ "hidden": false,
6524
+ "defaultValue": "<runtime-path>"
6525
+ }
6526
+ ],
6527
+ "subcommands": []
6528
+ },
6474
6529
  {
6475
6530
  "name": "sync",
6476
6531
  "description": "Run every hot channel now: each adapter once per connection you can use",
@@ -6728,6 +6783,11 @@ export const COMMAND_CATALOG = [
6728
6783
  "description": "Print the owner's run reply only when the run found new signals; print nothing otherwise",
6729
6784
  "hidden": false
6730
6785
  },
6786
+ {
6787
+ "flags": "--redo <file>",
6788
+ "description": "Process a source item again (sources/<channel>/<file>.md; repeatable; needs --company): the extract step re-queues it in the ontology worklist before listing",
6789
+ "hidden": false
6790
+ },
6731
6791
  {
6732
6792
  "flags": "--hq-root <path>",
6733
6793
  "description": "HQ root (default: <runtime-path>)",
@@ -21,6 +21,7 @@ interface RunCliOptions {
21
21
  maxBudgetUsd?: number;
22
22
  json?: boolean;
23
23
  reportFound?: boolean;
24
+ redo?: string[];
24
25
  hqRoot: string;
25
26
  }
26
27
  export declare function formatRunResult(result: RunSignalsResult, dryRun: boolean): string;
@@ -19,6 +19,9 @@ function parsePositiveInt(v) {
19
19
  throw new InvalidArgumentError("must be a positive integer");
20
20
  return n;
21
21
  }
22
+ function collectRedo(v, prev = []) {
23
+ return [...prev, v];
24
+ }
22
25
  function parseNonNegative(v) {
23
26
  const n = Number(v);
24
27
  if (!Number.isFinite(n) || n < 0)
@@ -113,6 +116,7 @@ export async function runSignalsCli(options, overrides = {}) {
113
116
  catchUp: options.catchUp === true || envCatchUp(),
114
117
  maxTurns: options.maxTurns,
115
118
  maxBudgetUsd: options.maxBudgetUsd,
119
+ ...(options.redo?.length ? { redo: options.redo } : {}),
116
120
  signalsAlert: overrides.signalsAlert ?? terminalSignalsAlert(),
117
121
  });
118
122
  const code = signalsExitCode(result);
@@ -155,6 +159,7 @@ export function registerSignalsRunSubcommand(signals) {
155
159
  .option("--max-budget-usd <x>", "Spend cap in USD for the extract step (default from core/settings/signals.yaml, else 2.00)", parseNonNegative)
156
160
  .option("--json", "Print the run record as JSON")
157
161
  .option("--report-found", "Print the owner's run reply only when the run found new signals; print nothing otherwise")
162
+ .option("--redo <file>", "Process a source item again (sources/<channel>/<file>.md; repeatable; needs --company): the extract step re-queues it in the ontology worklist before listing", collectRedo)
158
163
  .option("--hq-root <path>", `HQ root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
159
164
  .action(async (options) => {
160
165
  try {
@@ -128,6 +128,53 @@ export declare function runCompanyHotChannelSync(input: CompanyHotChannelSyncInp
128
128
  * line under `companies/{company}/sources/`, and nothing else.
129
129
  */
130
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
+ }
131
178
  export interface ScheduleOptions {
132
179
  company?: string;
133
180
  at?: 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,7 +35,8 @@ 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";
@@ -642,6 +644,139 @@ function renderSyncTable(company, report) {
642
644
  }
643
645
  return lines.join("\n");
644
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
+ }
645
780
  function fleetBotsIn(root) {
646
781
  let names;
647
782
  try {
@@ -842,6 +977,27 @@ export function registerSourcesCommand(program) {
842
977
  .action(withErrorHandling(async (options) => {
843
978
  await runSchedule(options);
844
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
+ }));
845
1001
  sources
846
1002
  .command("sync")
847
1003
  .description("Run every hot channel now: each adapter once per connection you can use")
@@ -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,
@@ -12,6 +12,12 @@ export interface HotChannelRunResult {
12
12
  /** Adapter id, or the catalog provider when it did not resolve. */
13
13
  adapterId: string;
14
14
  provider: string;
15
+ /**
16
+ * The adapter's channel (`sources/{channel}/`). Set once the adapter
17
+ * resolves, so the signals pass can drain that channel's worklist even
18
+ * when the run landed nothing.
19
+ */
20
+ channel?: string;
15
21
  /** The connection this run used; null for a channel with no connection. */
16
22
  connectionId: string | null;
17
23
  transport: HotTransport | null;
@@ -209,6 +209,7 @@ export async function runHotChannelLoop(input) {
209
209
  try {
210
210
  const adapter = input.registry.resolve(entry);
211
211
  result.adapterId = adapter.id;
212
+ result.channel = adapter.channel;
212
213
  result.transport = adapter.transport;
213
214
  if (adapter.transport === "push") {
214
215
  await drainPush(ctx, adapter, result);
@@ -1,9 +1,22 @@
1
1
  /**
2
- * The local signals pass (US-011). After a hot-channel run lands sources, the
3
- * pass takes the run's landed documents through the ontology worklist, asks
2
+ * The local signals pass (US-011). After each hot-channel adapter run, the
3
+ * pass takes the adapter's channel through the ontology worklist, asks
4
4
  * `claude -p` for signals one document at a time, and writes those signals to
5
5
  * `companies/{co}/signals/`. It returns the count for the run ledger.
6
6
  *
7
+ * Work comes from the worklist, not only from this run's landings: when the
8
+ * worklist script exits 0, the pass takes the channel's unprocessed items in
9
+ * the worklist's order (newest first) up to the run's document cap. Items the
10
+ * cap leaves over stay unmarked, and a later run picks them up, including a
11
+ * run that landed nothing (a bounded backlog drain: at most
12
+ * DEFAULT_MAX_SIGNAL_DOCS claude calls per run, and no claude call at all when
13
+ * the backlog is empty). A channel with no source.yaml (worklist exit 2, the
14
+ * usual case for a hot channel such as `meeting-events`) has no worklist, so
15
+ * the pass keeps its own backlog: landed documents it has not processed yet
16
+ * are listed in `sources/_signals-pending/{channel}.json` and drained, newest
17
+ * first by `when` (else `created_at`), on later runs under the same cap. A
18
+ * cloud-run source (exit 4) is skipped.
19
+ *
7
20
  * A landed document with both `payload_ref` and `raw_ref` is a pointer (the
8
21
  * meetings, Slack and Attio adapters land these). For it, claude gets the
9
22
  * pointer followed by the document `payload_ref` names, capped at
@@ -118,6 +131,24 @@ interface ParsedSignal {
118
131
  }
119
132
  export declare function defaultPromptFile(): string;
120
133
  export declare const CLAUDE_LOGGED_OUT_RE: RegExp;
134
+ export declare function channelOf(key: string): string | null;
135
+ /**
136
+ * Vault-relative file that lists a channel's landed documents the pass has not
137
+ * processed yet. Only channels with no worklist (no source.yaml) use it.
138
+ */
139
+ export declare function pendingSignalsKey(channel: string): string;
140
+ /** The channel's pending landed keys; a missing or unreadable file is an empty backlog. */
141
+ export declare function readPendingSignals(vaultRoot: string, channel: string): Promise<string[]>;
142
+ /** Replace the channel's pending list; an empty list removes the file. */
143
+ export declare function writePendingSignals(vaultRoot: string, channel: string, keys: readonly string[], beforeWrite?: () => Promise<void>): Promise<void>;
144
+ /** Add landed keys to the channel's pending list (kept once each). */
145
+ export declare function queuePendingSignals(vaultRoot: string, channel: string, keys: readonly string[], beforeWrite?: () => Promise<void>): Promise<void>;
146
+ /** When a landed document happened: `when`, else `created_at`; unknown sorts last. */
147
+ export declare function landedDocTime(frontmatter: Record<string, unknown>): number;
148
+ export declare function splitFrontmatter(text: string): {
149
+ frontmatter: Record<string, unknown>;
150
+ body: string;
151
+ };
121
152
  /**
122
153
  * The text claude gets for one landed document. A pointer (both `payload_ref`
123
154
  * and `raw_ref` set) gets its payload appended after a separator line. Any