@indigoai-us/hq-cli 5.345.193 → 5.345.195
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 +67 -0
- package/dist/command-catalog.generated.d.ts +49 -0
- package/dist/command-catalog.generated.js +60 -0
- package/dist/commands/bot.js +200 -29
- 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/bot/fleet-local.d.ts +3 -1
- package/dist/lib/bot/fleet-local.js +12 -11
- package/dist/lib/bot/legacy-convert.d.ts +15 -0
- package/dist/lib/bot/legacy-convert.js +38 -0
- 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/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,
|
|
@@ -324,7 +324,9 @@ export declare function pipeInheritedStdio(stdio: unknown): {
|
|
|
324
324
|
*/
|
|
325
325
|
export declare function windowsExtractionPathProblem(destination: string, longest: number, longPathsEnabled: number | null): string | null;
|
|
326
326
|
/** Moves a fresh machine credential into the bot's secure store and deletes the file. */
|
|
327
|
-
export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps
|
|
327
|
+
export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps, opts?: {
|
|
328
|
+
keepSource?: boolean;
|
|
329
|
+
}): void;
|
|
328
330
|
export declare function bootstrapFleetLocalBot(input: FleetLocalProfileInput & {
|
|
329
331
|
machineCredentials?: string;
|
|
330
332
|
}, deps?: FleetLocalBootstrapDeps): Promise<FleetLocalBootstrapResult>;
|
|
@@ -79,15 +79,15 @@ export function sharedRuntimeLayout(root, id, platform) {
|
|
|
79
79
|
}
|
|
80
80
|
export const HQ_FLEET_LOCAL_RELEASE = {
|
|
81
81
|
repository: "indigoai-us/hq-fleet-local",
|
|
82
|
-
tag: "v0.4.
|
|
83
|
-
version: "0.4.
|
|
84
|
-
artifact: "hq-fleet-local-0.4.
|
|
85
|
-
artifactSha256: "
|
|
86
|
-
runtimeArtifact: "hq-fleet-runtime-
|
|
87
|
-
runtimeArtifactSha256: "
|
|
88
|
-
manifestSha256: "
|
|
89
|
-
signatureSha256: "
|
|
90
|
-
runtimeCommit: "
|
|
82
|
+
tag: "v0.4.21",
|
|
83
|
+
version: "0.4.21",
|
|
84
|
+
artifact: "hq-fleet-local-0.4.21.zip",
|
|
85
|
+
artifactSha256: "24162dcc733fdc4a8e7a3482f5b2c445f5b6af96acf1695dd7dc5c7803272e3f",
|
|
86
|
+
runtimeArtifact: "hq-fleet-runtime-24211b85a414.tar",
|
|
87
|
+
runtimeArtifactSha256: "9084d88147bc9d70052aa00cc963eec2f80525bf61539d22abf45497712bd8b2",
|
|
88
|
+
manifestSha256: "5b52bd4f071470941ab1cd9a4f6baf614179f70073f60bfdf63f8a7ba38aa7ad",
|
|
89
|
+
signatureSha256: "85c35d5b2ca365503c408cd83a6d19758382f2e3049840fa055860e6f2d21746",
|
|
90
|
+
runtimeCommit: "24211b85a414441bac6405d862b1c872b44a78a1",
|
|
91
91
|
};
|
|
92
92
|
/** hq-fleet-local trust/release-public.pem (rotated 2026-10-30; signed v0.4.17). */
|
|
93
93
|
export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
@@ -1585,7 +1585,7 @@ function assertWindowsExtractionFits(run, python, archive, destination) {
|
|
|
1585
1585
|
throw expectedError(problem);
|
|
1586
1586
|
}
|
|
1587
1587
|
/** Moves a fresh machine credential into the bot's secure store and deletes the file. */
|
|
1588
|
-
export function importFleetLocalMachineCredential(agentUid, machineCredentials, deps = {}) {
|
|
1588
|
+
export function importFleetLocalMachineCredential(agentUid, machineCredentials, deps = {}, opts = {}) {
|
|
1589
1589
|
const run = deps.run ?? execFileSync;
|
|
1590
1590
|
const files = fleetLocalExecutables(agentUid, deps);
|
|
1591
1591
|
const machineValue = fs.readFileSync(machineCredentials, "utf8").trim();
|
|
@@ -1600,7 +1600,8 @@ export function importFleetLocalMachineCredential(agentUid, machineCredentials,
|
|
|
1600
1600
|
"--bot-id",
|
|
1601
1601
|
agentUid,
|
|
1602
1602
|
], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
|
|
1603
|
-
|
|
1603
|
+
if (!opts.keepSource)
|
|
1604
|
+
fs.rmSync(machineCredentials, { force: true });
|
|
1604
1605
|
}
|
|
1605
1606
|
async function installSharedFleetRuntime(installation, root, python, platform, run, spawn) {
|
|
1606
1607
|
const identity = digest(Buffer.from(JSON.stringify({
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type BotConfig } from "./config.js";
|
|
2
|
+
export declare const KEEP_LEGACY_RUNTIME_ENV = "HQ_BOT_KEEP_LEGACY_RUNTIME";
|
|
3
|
+
export type LegacyConversionDecision = {
|
|
4
|
+
convert: true;
|
|
5
|
+
} | {
|
|
6
|
+
convert: false;
|
|
7
|
+
reason: "already-fleet" | "kept-by-env" | "needs-sign-in" | "own-claude-account";
|
|
8
|
+
};
|
|
9
|
+
export declare function legacyConversionDecision(config: Pick<BotConfig, "engine" | "runtime" | "runtimeAuth">, env?: NodeJS.ProcessEnv): LegacyConversionDecision;
|
|
10
|
+
/** How long a legacy runner's own start skips the move after one failed. */
|
|
11
|
+
export declare const FLEET_MOVE_RETRY_MS: number;
|
|
12
|
+
export declare function fleetMoveFailurePath(dir: string): string;
|
|
13
|
+
export declare function writeFleetMoveFailure(dir: string, now?: Date): void;
|
|
14
|
+
export declare function fleetMoveRecentlyFailed(dir: string, now?: number): boolean;
|
|
15
|
+
//# sourceMappingURL=legacy-convert.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { HQ_FLEET_RUNTIME_ENGINE_ID } from "./config.js";
|
|
4
|
+
export const KEEP_LEGACY_RUNTIME_ENV = "HQ_BOT_KEEP_LEGACY_RUNTIME";
|
|
5
|
+
export function legacyConversionDecision(config, env = process.env) {
|
|
6
|
+
if (config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID)
|
|
7
|
+
return { convert: false, reason: "already-fleet" };
|
|
8
|
+
if (env[KEEP_LEGACY_RUNTIME_ENV] === "1")
|
|
9
|
+
return { convert: false, reason: "kept-by-env" };
|
|
10
|
+
if (config.runtime !== "claude")
|
|
11
|
+
return { convert: false, reason: "needs-sign-in" };
|
|
12
|
+
if (config.runtimeAuth?.claude)
|
|
13
|
+
return { convert: false, reason: "own-claude-account" };
|
|
14
|
+
return { convert: true };
|
|
15
|
+
}
|
|
16
|
+
/** How long a legacy runner's own start skips the move after one failed. */
|
|
17
|
+
export const FLEET_MOVE_RETRY_MS = 60 * 60 * 1000;
|
|
18
|
+
export function fleetMoveFailurePath(dir) {
|
|
19
|
+
return path.join(dir, "fleet-move-failed.json");
|
|
20
|
+
}
|
|
21
|
+
export function writeFleetMoveFailure(dir, now = new Date()) {
|
|
22
|
+
try {
|
|
23
|
+
fs.writeFileSync(fleetMoveFailurePath(dir), JSON.stringify({ at: now.toISOString() }) + "\n", { mode: 0o600 });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* the next start just tries again */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function fleetMoveRecentlyFailed(dir, now = Date.now()) {
|
|
30
|
+
try {
|
|
31
|
+
const at = Date.parse(JSON.parse(fs.readFileSync(fleetMoveFailurePath(dir), "utf8")).at ?? "");
|
|
32
|
+
return Number.isFinite(at) && now - at < FLEET_MOVE_RETRY_MS;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=legacy-convert.js.map
|
|
@@ -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
|