@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 +33 -0
- package/dist/command-catalog.generated.d.ts +49 -0
- package/dist/command-catalog.generated.js +60 -0
- package/dist/commands/signals-run.d.ts +1 -0
- package/dist/commands/signals-run.js +5 -0
- package/dist/commands/sources.d.ts +47 -0
- package/dist/commands/sources.js +157 -1
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +35 -2
- package/dist/lib/hot-channels/loop.d.ts +6 -0
- package/dist/lib/hot-channels/loop.js +1 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +33 -2
- package/dist/lib/hot-channels/signals-pass.js +142 -33
- package/dist/lib/hot-channels/signals-redo.d.ts +60 -0
- package/dist/lib/hot-channels/signals-redo.js +207 -0
- package/dist/lib/signals/engine.d.ts +6 -0
- package/dist/lib/signals/engine.js +10 -1
- package/dist/lib/signals/steps/extract.d.ts +5 -0
- package/dist/lib/signals/steps/extract.js +30 -2
- package/dist/lib/signals/steps/types.d.ts +5 -0
- package/dist/utils/vault-api.d.ts +1 -0
- package/dist/utils/vault-api.js +1 -1
- package/package.json +7 -7
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>)",
|
|
@@ -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;
|
package/dist/commands/sources.js
CHANGED
|
@@ -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;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
3
|
-
* pass takes the
|
|
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
|