@indigoai-us/hq-cli 5.345.200 → 5.345.201
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 +14 -0
- package/dist/commands/bot.d.ts +47 -0
- package/dist/commands/bot.js +200 -20
- package/dist/commands/cloud.d.ts +39 -0
- package/dist/commands/cloud.js +173 -82
- package/dist/commands/deploy-domains.js +8 -4
- package/dist/lib/bot/api.d.ts +8 -0
- package/dist/lib/bot/api.js +9 -0
- package/dist/lib/bot/fleet-local.d.ts +28 -0
- package/dist/lib/bot/fleet-local.js +75 -6
- package/dist/lib/bot/jobs/source-report.d.ts +31 -0
- package/dist/lib/bot/jobs/source-report.js +276 -0
- package/dist/lib/bot/legacy-convert.d.ts +107 -4
- package/dist/lib/bot/legacy-convert.js +159 -13
- package/dist/lib/bot/legacy-provider-login.d.ts +34 -0
- package/dist/lib/bot/legacy-provider-login.js +144 -0
- package/dist/lib/bot/run.js +20 -1
- package/dist/lib/doctor/checks/bots.d.ts +4 -0
- package/dist/lib/doctor/checks/bots.js +33 -0
- package/dist/lib/lanes/store.js +7 -0
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/lanes/watcher.d.ts +0 -33
- package/dist/lib/lanes/watcher.js +7 -0
- package/dist/utils/resident-package-preload.js +57 -4
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.201] — 2026-11-04
|
|
6
|
+
- `hq deploy domains buy` accepts a registrar price in `usd` or `USD`. Search shows that price as US dollars, and renew accepts the same code. A price in another currency is still refused, and the message shows the currency code the server sent.
|
|
7
|
+
- The Executive sends source reports to its owner when a meeting ends and one final threaded report after all source lanes finish. Delivery receipts suppress normal duplicates and retain reports during the ten-minute cooldown. Recovery retries an intent without a sent receipt; a crash after sending but before recording the receipt can resend that report.
|
|
8
|
+
- Re-pin native policy enforcement to the corrected staging gate, including keyed host approvals and consumed fact proofs. Fix CRLF transport in Windows answer rows, receipt names and required fact names. Recognize drive-qualified and MSYS absolute read paths while preserving exact receipt names. The port remains opt-in and requires the matching core script.
|
|
9
|
+
- `hq-hook`: `block-agent-secrets-reveal` has a full native port (US-011). `hq-hook bridge block-agent-secrets-reveal` answers every call of the hq-core-staging guard (sha256 `e1a1b05d`) itself, including denies, and matches the bash guard byte for byte on macOS and Windows. It runs the flag reader with node only when the command is a secret reveal, as the guard does. The flag `hook-engine-port-block-agent-secrets-reveal` stays off, so nothing changes until it is turned on.
|
|
10
|
+
- `hq-hook`: port `lanes-repo-merge-hold` payload validation, merge prefilter, dependency checks and hold-response formatting to native Rust. Preserve the bounded CLI call and its failure notices, including timeouts without a hold record. Convert Git Bash TMPDIR drive paths through the shared MSYS converter. Capture CLI output in private files so inherited child handles cannot extend the timeout wait. The port flag stays off.
|
|
11
|
+
- Older local bots on the Codex or Grok runner now move to the HQ Fleet Runtime at their next start when their provider sign-in is readable on the computer: the Codex CLI login (or the bot's own Codex login) is imported, and a Grok login is copied from another Fleet bot on the same computer. A bot whose sign-in is not readable stays on its legacy runner, records why, files one failure report with HQ per reason (the same route a Fleet bot reports a failed turn through), and DMs its owner once per reason with the `hq bot auth <name> --provider <chatgpt|grok>` fix. A Claude bot with its own Claude account stays and tells the owner that `hq bot auth <name> --provider claude` moves it onto the owner's Claude Code login. `hq bot list --json`, `hq bot status --json` and `hq bot start --json` now carry `brainSignIn` (the provider a bot of any engine must sign in to before it can run, with the command) and, for a legacy bot, `legacy` (whether it moves at its next start, the hold reason, the last failed move, the owner DM and the report id); `hq bot status` and `hq doctor --only bots` say the same in words. `HQ_BOT_KEEP_LEGACY_RUNTIME=1` still keeps any bot legacy.
|
|
12
|
+
- Local bots now install HQ Fleet Local 0.4.26 on HQ Fleet Runtime main d257fdd1
|
|
13
|
+
(same gzip runtime archive as 0.4.25). A bot whose turn failed no longer counts its
|
|
14
|
+
own "I'm away for a bit" reply as a worked turn, so `hq bot status` and `hq doctor`
|
|
15
|
+
keep showing the failure and its fix until the bot really answers. Installed bots
|
|
16
|
+
move to 0.4.26 at the daemon's next update pass.
|
|
17
|
+
- `hq sync pull` now recovers when the HQ server rejects a sign-in that still looks valid on your machine (`hq auth status` and `hq auth refresh` only check the saved expiry): it refreshes the saved session once and retries, and if the server still rejects it the message tells you to run `hq login` instead of printing a bare "Unauthorized". Waiting on the sign-in service during a sync now stops after 45 seconds with a clear message.
|
|
18
|
+
|
|
5
19
|
## [5.345.200] — 2026-11-04
|
|
6
20
|
- `hq creators apply`, `hq library` (search, add, describe, link, rm),
|
|
7
21
|
`hq sync held`, and `hq lanes` no longer open a browser sign-in when no
|
package/dist/commands/bot.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LegacyMoveState, type LegacyProviderLoginProbe } from "../lib/bot/legacy-convert.js";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { type RuntimeSignInCheck, type RuntimeSignInIssue } from "../lib/bot/runtime-sign-in.js";
|
|
3
4
|
import { type ClaudeLoginCheck } from "../lib/bot/claude-login.js";
|
|
@@ -62,6 +63,16 @@ export interface BotRow {
|
|
|
62
63
|
runtimeSignedIn: boolean | null;
|
|
63
64
|
/** The terminal command that signs the bot's coding tool in on this computer. */
|
|
64
65
|
signInCommand: string;
|
|
66
|
+
/**
|
|
67
|
+
* Set when the bot cannot run until its model provider is signed in, on any
|
|
68
|
+
* engine: a Fleet bot never signed in or whose last turn failed on auth, a
|
|
69
|
+
* legacy bot held for a sign-in it cannot import, or a legacy runner that
|
|
70
|
+
* found its tool signed out. `command` signs the bot in (and moves a legacy
|
|
71
|
+
* bot to the HQ Fleet Runtime); null when no sign-in is needed.
|
|
72
|
+
*/
|
|
73
|
+
brainSignIn: BrainSignIn | null;
|
|
74
|
+
/** A bot still on its legacy runner: whether and why it moves at its next start. Absent for a Fleet bot. */
|
|
75
|
+
legacy?: Record<string, unknown>;
|
|
65
76
|
/**
|
|
66
77
|
* The coding tool crashed before its turn started since the bot last
|
|
67
78
|
* started, and no turn has worked since; null otherwise. A restart clears it.
|
|
@@ -113,8 +124,40 @@ export declare function ownerSignInAllowed(): boolean;
|
|
|
113
124
|
* sign-out, and the next start checks afresh.
|
|
114
125
|
*/
|
|
115
126
|
export declare function runtimeSignedIn(status: Pick<BotStatusFile, "runtimeSignIn" | "runtimeSignInCheck"> | null | undefined, processAlive: boolean): boolean | null;
|
|
127
|
+
export interface BrainSignIn {
|
|
128
|
+
provider: BotRuntimeId;
|
|
129
|
+
needed: true;
|
|
130
|
+
reason: string;
|
|
131
|
+
command: string;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether the bot's model provider must be signed in before it can run, from
|
|
135
|
+
* what this computer knows: the bot's settings, its last reported failure or
|
|
136
|
+
* sign-in check, and (legacy) why it has not moved to the HQ Fleet Runtime.
|
|
137
|
+
*/
|
|
138
|
+
export declare function brainSignInFor(name: string, config: BotConfig, status: BotStatusFile | null, legacy: LegacyMoveState | null): BrainSignIn | null;
|
|
116
139
|
/** Spawn `hq bot run <name>` detached (no launchd). */
|
|
117
140
|
export declare function spawnDetachedRun(name: string, dir: string): number;
|
|
141
|
+
/**
|
|
142
|
+
* Move a legacy bot onto the HQ Fleet Runtime under the same identity.
|
|
143
|
+
* Returns the bot's new settings, or null when it stays on the legacy runner
|
|
144
|
+
* (it needs its own sign-in, the move is turned off, or the move failed, in
|
|
145
|
+
* which case the next start tries again).
|
|
146
|
+
*
|
|
147
|
+
* bot.json switches to Fleet only after the Fleet service is installed, so a
|
|
148
|
+
* failure or a kill at any earlier step leaves a legacy bot that starts as
|
|
149
|
+
* before. Legacy jobs move and the legacy startup agent goes after that.
|
|
150
|
+
*
|
|
151
|
+
* `from: "start"` stops the legacy runner first; a failed move leaves it
|
|
152
|
+
* stopped and the caller starts it again. `from: "run"` is the legacy runner
|
|
153
|
+
* itself starting (launchd, hq daemon or a detached run), before it polls:
|
|
154
|
+
* its own startup agent is booted out last, detached.
|
|
155
|
+
*/
|
|
156
|
+
/** Seams for the legacy move, replaced in tests. */
|
|
157
|
+
export declare const legacyConvertSeams: {
|
|
158
|
+
/** Whether the provider sign-in a Codex or Grok legacy bot needs is readable here, as HQ Fleet Local imports it. */
|
|
159
|
+
providerLogin: (dir: string, config: BotConfig) => LegacyProviderLoginProbe;
|
|
160
|
+
};
|
|
118
161
|
/**
|
|
119
162
|
* `output`: "json" | "text" print a result; "silent" prints nothing — used
|
|
120
163
|
* when stop is a step inside rm/restart, whose own JSON must be the only
|
|
@@ -188,6 +231,10 @@ export declare function runBotCompanies(nameArg: string, opts: {
|
|
|
188
231
|
export declare function runBotWorkers(opts: {
|
|
189
232
|
json?: boolean;
|
|
190
233
|
}): Promise<void>;
|
|
234
|
+
/** The `legacy` object in `hq bot status --json` for a bot still on its legacy runner. */
|
|
235
|
+
export declare function legacyStatusJson(state: LegacyMoveState): Record<string, unknown>;
|
|
236
|
+
/** The `fleet move:` lines of `hq bot status` for a bot still on its legacy runner. */
|
|
237
|
+
export declare function legacyStatusLines(name: string, state: LegacyMoveState): string[];
|
|
191
238
|
/**
|
|
192
239
|
* `hq bot set <name> [--model <id|default>] [--effort <level|default>]
|
|
193
240
|
* [--display-name <name>]`: change what the bot thinks with, or the name
|
package/dist/commands/bot.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { withLocalJobs } from "../lib/bot/local-jobs.js";
|
|
2
|
-
import {
|
|
2
|
+
import { clearFleetMoveRecords, fleetMoveHoldPath, fleetMoveRecentlyFailed, holdReportDue, legacyConversionDecision, legacyHoldFailureReport, legacyHoldNotice, legacyHoldNotifiesOwner, legacyMoveState, ownerHoldNoticeDue, readFleetMoveFailure, readFleetMoveHold, writeFleetMoveFailure, writeFleetMoveHold, } from "../lib/bot/legacy-convert.js";
|
|
3
|
+
import { legacyProviderLoginProbe } from "../lib/bot/legacy-provider-login.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { CLI_VERSION } from "../cli-version.js";
|
|
3
6
|
import { runSignals } from "../lib/signals/engine.js";
|
|
4
7
|
import { BOT_SIGNALS_ENGINE_JOB_FLAG, signalsEngineForJob } from "../lib/bot/signals-engine-flag.js";
|
|
5
8
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
@@ -70,7 +73,7 @@ import { unresolvedLastFailure } from "../lib/doctor/checks/bots.js";
|
|
|
70
73
|
import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
|
|
71
74
|
import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
|
|
72
75
|
import { BOT_LOGIN_RUNTIMES, botAuthEnv, botRuntimeAuth, capacityAccountHome, describeBotAuth, formatBotAuth, isBotLoginRuntime, restoreBotSessions, setAsideBotSessions, signInBotRuntime, signOutBotRuntime, withRuntimeAuth, } from "../lib/bot/runtime-auth.js";
|
|
73
|
-
import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, HQ_FLEET_LOCAL_RELEASE, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetLocalServiceInstalled, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetOwnerCompanies, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, installTargetFleetLocalDistribution, resolveFleetLocalPointerTarget, resolveFleetLocalTargetRelease, fleetLocalProfileDir, fleetProfileOwnerCompanies, fleetLocalChannels, readFleetLocalGatewayState, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
|
|
76
|
+
import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, HQ_FLEET_LOCAL_RELEASE, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetLocalServiceInstalled, fleetProviderForRuntime, importFleetLocalMachineCredential, importFleetLocalProviderLogin, fleetProviderNeedsSignIn, fleetOwnerCompanies, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, installTargetFleetLocalDistribution, resolveFleetLocalPointerTarget, resolveFleetLocalTargetRelease, fleetLocalProfileDir, fleetProfileOwnerCompanies, fleetLocalChannels, readFleetLocalGatewayState, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
|
|
74
77
|
import { gatedInteractiveLogin } from "../utils/interactive-caller.js";
|
|
75
78
|
import { fleetInstallProgressReporter, runBotRuntimeInstall, runBotRuntimeStatus } from "./bot-runtime.js";
|
|
76
79
|
function collectRepeated(value, previous) {
|
|
@@ -368,11 +371,39 @@ export function runtimeSignedIn(status, processAlive) {
|
|
|
368
371
|
return false;
|
|
369
372
|
return status.runtimeSignInCheck?.signedIn ?? null;
|
|
370
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Whether the bot's model provider must be signed in before it can run, from
|
|
376
|
+
* what this computer knows: the bot's settings, its last reported failure or
|
|
377
|
+
* sign-in check, and (legacy) why it has not moved to the HQ Fleet Runtime.
|
|
378
|
+
*/
|
|
379
|
+
export function brainSignInFor(name, config, status, legacy) {
|
|
380
|
+
const provider = config.runtime;
|
|
381
|
+
const command = fleetAuthCommand(name, config);
|
|
382
|
+
if (legacy && !legacy.decision.convert && legacyHoldNotifiesOwner(legacy.decision.reason)) {
|
|
383
|
+
return { provider, needed: true, reason: legacy.notice?.cause ?? legacy.decision.detail, command };
|
|
384
|
+
}
|
|
385
|
+
if (config.fleetAuthPending === true)
|
|
386
|
+
return { provider, needed: true, reason: `${name} has not signed in to ${runtimeDisplayName(provider)} yet.`, command };
|
|
387
|
+
// A legacy bot that moves at its next start needs no sign-in, whatever its old runner last reported.
|
|
388
|
+
if (legacy?.decision.convert)
|
|
389
|
+
return null;
|
|
390
|
+
const failure = unresolvedLastFailure(status);
|
|
391
|
+
if (failure?.class === "auth")
|
|
392
|
+
return { provider, needed: true, reason: failure.cause, command };
|
|
393
|
+
if (status?.runtimeSignIn)
|
|
394
|
+
return { provider, needed: true, reason: signedOutReason(name, status.runtimeSignIn), command };
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
function signedOutReason(name, issue) {
|
|
398
|
+
const tool = runtimeDisplayName(issue.runtime);
|
|
399
|
+
return issue.state === "signed-out" ? `${tool} is not signed in for ${name} on this computer.` : `${tool} needs ${name}'s owner to sign in again on this computer.`;
|
|
400
|
+
}
|
|
371
401
|
function rowFor(name, opts = {}) {
|
|
372
402
|
const dir = botDir(name);
|
|
373
403
|
const config = readBotConfig(dir);
|
|
374
404
|
const fleet = isFleetRuntimeBot(config);
|
|
375
405
|
const status = readBotStatus(dir);
|
|
406
|
+
const legacy = fleet ? null : opts.legacy ?? legacyMoveState(dir, config, legacyConvertSeams.providerLogin(dir, config));
|
|
376
407
|
const pid = fleet ? fleetLocalGatewayPid(config.agentUid) : (status?.pid ?? null);
|
|
377
408
|
const processAlive = pid !== null && isPidAlive(pid) && (fleet || status?.state === "running");
|
|
378
409
|
// Turns this bot has taken and not finished: what "thinking" means.
|
|
@@ -419,6 +450,8 @@ function rowFor(name, opts = {}) {
|
|
|
419
450
|
...(fleet && config.fleetAuthPending === true ? { needsSignIn: true, authCommand: fleetAuthCommand(name, config) } : {}),
|
|
420
451
|
...(fleet && config.fleetServiceInstallPending === true ? { needsSignIn: false, serviceInstallPending: true } : {}),
|
|
421
452
|
signInCommand: fleet ? fleetAuthCommand(name, config) : signInCommand(config.runtime),
|
|
453
|
+
brainSignIn: brainSignInFor(name, config, status, legacy),
|
|
454
|
+
...(legacy ? { legacy: legacyStatusJson(legacy) } : {}),
|
|
422
455
|
runtimeCrash: processAlive ? runtimeCrashSinceStart(status) : null,
|
|
423
456
|
daemonInstalled,
|
|
424
457
|
daemonLoaded,
|
|
@@ -525,9 +558,12 @@ async function startBot(name, dir, config, output, extra = {}, opts = {}) {
|
|
|
525
558
|
throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
|
|
526
559
|
if (isFleetRuntimeBot(config))
|
|
527
560
|
return startFleetRuntimeBot(name, dir, config, output, extra);
|
|
528
|
-
const
|
|
529
|
-
if (
|
|
530
|
-
return reportFleetBotStart(name, dir,
|
|
561
|
+
const move = opts.keepLegacy ? null : await convertLegacyBotToFleet(name, dir, config, "start");
|
|
562
|
+
if (move?.fleet)
|
|
563
|
+
return reportFleetBotStart(name, dir, move.fleet, output, { ...extra, movedToFleet: true }, true, "started by the HQ Fleet Runtime");
|
|
564
|
+
// The app reads these to say the bot is still legacy and, when a sign-in is all it waits for, to offer it.
|
|
565
|
+
if (move?.state)
|
|
566
|
+
extra = { ...extra, movedToFleet: false, legacy: legacyStatusJson(move.state), brainSignIn: brainSignInFor(name, config, readBotStatus(dir), move.state) };
|
|
531
567
|
if (config.enabled === false)
|
|
532
568
|
patchBotConfig(dir, { enabled: true });
|
|
533
569
|
const paths = buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot });
|
|
@@ -604,21 +640,37 @@ async function startBot(name, dir, config, output, extra = {}, opts = {}) {
|
|
|
604
640
|
* itself starting (launchd, hq daemon or a detached run), before it polls:
|
|
605
641
|
* its own startup agent is booted out last, detached.
|
|
606
642
|
*/
|
|
643
|
+
/** Seams for the legacy move, replaced in tests. */
|
|
644
|
+
export const legacyConvertSeams = {
|
|
645
|
+
/** Whether the provider sign-in a Codex or Grok legacy bot needs is readable here, as HQ Fleet Local imports it. */
|
|
646
|
+
providerLogin: (dir, config) => legacyProviderLoginProbe(dir, config),
|
|
647
|
+
};
|
|
607
648
|
async function convertLegacyBotToFleet(name, dir, config, from) {
|
|
608
|
-
if (
|
|
609
|
-
return null;
|
|
610
|
-
// A launchd or daemon restart loop would otherwise retry a failing install before every start.
|
|
649
|
+
if (hasPromotionHold(dir))
|
|
650
|
+
return { fleet: null, state: null };
|
|
651
|
+
// A launchd or daemon restart loop would otherwise retry a failing install (and probe the sign-in) before every start.
|
|
611
652
|
if (from === "run" && fleetMoveRecentlyFailed(dir))
|
|
612
|
-
return null;
|
|
653
|
+
return { fleet: null, state: null };
|
|
654
|
+
const probe = legacyConvertSeams.providerLogin(dir, config);
|
|
655
|
+
const decision = legacyConversionDecision(config, process.env, probe);
|
|
656
|
+
if (!decision.convert) {
|
|
657
|
+
if (legacyHoldNotifiesOwner(decision.reason))
|
|
658
|
+
await holdLegacyBot(name, dir, config, decision);
|
|
659
|
+
return { fleet: null, state: { decision, notice: legacyHoldNotice(name, config.runtime, decision), lastFailure: readFleetMoveFailure(dir), hold: readFleetMoveHold(dir) } };
|
|
660
|
+
}
|
|
661
|
+
// The hold is over: whatever stops the move from here is a failure, not a hold.
|
|
662
|
+
fs.rmSync(fleetMoveHoldPath(dir), { force: true });
|
|
663
|
+
const ready = () => ({ fleet: null, state: { decision, notice: null, lastFailure: readFleetMoveFailure(dir), hold: null } });
|
|
613
664
|
if (from === "start") {
|
|
614
665
|
const running = readBotStatus(dir);
|
|
615
666
|
if (running && isPidAlive(running.pid) && selfStopProbe(running.pid))
|
|
616
|
-
return
|
|
667
|
+
return ready();
|
|
617
668
|
}
|
|
618
669
|
const log = createBotLogger(dir);
|
|
619
670
|
const keepLegacy = (step, err) => {
|
|
620
|
-
|
|
621
|
-
|
|
671
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
672
|
+
writeFleetMoveFailure(dir, new Date(), { step, error });
|
|
673
|
+
log("warn", `could not ${step} ${name} on the HQ Fleet Runtime (${error}); it runs on ${config.runtime} this time and tries again at its next start`);
|
|
622
674
|
};
|
|
623
675
|
let engine;
|
|
624
676
|
try {
|
|
@@ -628,9 +680,27 @@ async function convertLegacyBotToFleet(name, dir, config, from) {
|
|
|
628
680
|
}
|
|
629
681
|
catch (err) {
|
|
630
682
|
keepLegacy("install", err);
|
|
631
|
-
return
|
|
683
|
+
return ready();
|
|
684
|
+
}
|
|
685
|
+
if (decision.signIn) {
|
|
686
|
+
try {
|
|
687
|
+
importFleetLocalProviderLogin(config.agentUid, decision.signIn.runtime, {}, decision.signIn.env ? { env: decision.signIn.env } : {});
|
|
688
|
+
log("info", `imported ${name}'s ${config.runtime} sign-in from this computer for the HQ Fleet Runtime`);
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
keepLegacy("sign in", err);
|
|
692
|
+
return ready();
|
|
693
|
+
}
|
|
632
694
|
}
|
|
633
|
-
const
|
|
695
|
+
const current = readBotConfig(dir) ?? config;
|
|
696
|
+
const fleet = {
|
|
697
|
+
...current,
|
|
698
|
+
engine,
|
|
699
|
+
enabled: true,
|
|
700
|
+
fleetServiceInstallPending: false,
|
|
701
|
+
// The imported login is the bot's Fleet sign-in now; the legacy own-login setting no longer applies.
|
|
702
|
+
...(decision.signIn?.env ? { runtimeAuth: withRuntimeAuth(current.runtimeAuth, decision.signIn.runtime, null) } : {}),
|
|
703
|
+
};
|
|
634
704
|
if (from === "start")
|
|
635
705
|
await stopBot(name, dir, config, "silent");
|
|
636
706
|
let failure = null;
|
|
@@ -651,16 +721,77 @@ async function convertLegacyBotToFleet(name, dir, config, from) {
|
|
|
651
721
|
/* nothing was running */
|
|
652
722
|
}
|
|
653
723
|
keepLegacy("start", failure);
|
|
654
|
-
return
|
|
724
|
+
return ready();
|
|
655
725
|
}
|
|
656
726
|
writeBotConfig(dir, fleet);
|
|
657
|
-
|
|
727
|
+
clearFleetMoveRecords(dir);
|
|
658
728
|
// The legacy runner's copy of the machine credentials goes only now: a failed move above still needs it.
|
|
659
729
|
fs.rmSync(botCredsPath(dir), { force: true });
|
|
660
730
|
log("info", `moved ${name} to the HQ Fleet Runtime under the same identity; its ${config.runtime} runner is retired`);
|
|
661
731
|
syncFleetRuntimeJobs(name, dir, fleet);
|
|
662
732
|
retireLegacyBotAgent(name, dir, config, from === "run" ? "detached" : "inline");
|
|
663
|
-
return fleet;
|
|
733
|
+
return { fleet, state: null };
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* A legacy bot that cannot move yet: record why, and tell the owner once per
|
|
737
|
+
* reason through the bot's own DM, the way a Fleet bot reports a failed turn.
|
|
738
|
+
* The record is what `hq bot status` and `hq doctor` show.
|
|
739
|
+
*/
|
|
740
|
+
async function holdLegacyBot(name, dir, config, decision) {
|
|
741
|
+
const log = createBotLogger(dir);
|
|
742
|
+
const notice = legacyHoldNotice(name, config.runtime, decision);
|
|
743
|
+
const previous = readFleetMoveHold(dir);
|
|
744
|
+
const now = new Date().toISOString();
|
|
745
|
+
const hold = {
|
|
746
|
+
reason: decision.reason,
|
|
747
|
+
detail: decision.detail,
|
|
748
|
+
at: now,
|
|
749
|
+
...(previous?.notifiedAt ? { notifiedAt: previous.notifiedAt } : {}),
|
|
750
|
+
...(previous?.notifiedReason ? { notifiedReason: previous.notifiedReason } : {}),
|
|
751
|
+
...(previous?.reportId ? { reportId: previous.reportId } : {}),
|
|
752
|
+
...(previous?.reportedReason ? { reportedReason: previous.reportedReason } : {}),
|
|
753
|
+
};
|
|
754
|
+
writeFleetMoveHold(dir, hold);
|
|
755
|
+
log("warn", `${name} stays on its ${config.runtime} runner: ${notice.cause} ${notice.fix}`);
|
|
756
|
+
const reportDue = holdReportDue(previous, decision.reason);
|
|
757
|
+
const noticeDue = ownerHoldNoticeDue(previous, decision.reason);
|
|
758
|
+
if (!reportDue && !noticeDue)
|
|
759
|
+
return;
|
|
760
|
+
const api = new BotApi({ token: botTokenSupplier(dir) });
|
|
761
|
+
// One failure report per reason, the same route a Fleet bot files a failed turn through, so stuck legacy bots are visible in HQ.
|
|
762
|
+
if (reportDue) {
|
|
763
|
+
try {
|
|
764
|
+
const report = legacyHoldFailureReport({
|
|
765
|
+
reportId: randomUUID(),
|
|
766
|
+
createdAt: now,
|
|
767
|
+
name,
|
|
768
|
+
config,
|
|
769
|
+
decision,
|
|
770
|
+
notice,
|
|
771
|
+
machine: os.hostname().trim(),
|
|
772
|
+
hqCliVersion: CLI_VERSION,
|
|
773
|
+
os: `${process.platform} ${os.release()}`,
|
|
774
|
+
logPath: botLogPath(dir),
|
|
775
|
+
});
|
|
776
|
+
const { reportId } = await api.reportFailure(config.agentUid, report);
|
|
777
|
+
Object.assign(hold, { reportedReason: decision.reason, ...(reportId ? { reportId } : {}) });
|
|
778
|
+
writeFleetMoveHold(dir, hold);
|
|
779
|
+
log("info", `filed a failure report with HQ for ${name}'s held move (${decision.reason}${reportId ? `, report ${reportId}` : ""})`);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
log("warn", `could not file ${name}'s held move with HQ (${err instanceof Error ? err.message : String(err)}); the next start tries again`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (!noticeDue)
|
|
786
|
+
return;
|
|
787
|
+
try {
|
|
788
|
+
await api.sendDm({ toPersonUid: config.ownerUid, body: `${notice.cause} ${notice.fix}` });
|
|
789
|
+
writeFleetMoveHold(dir, { ...hold, notifiedAt: now, notifiedReason: decision.reason });
|
|
790
|
+
log("info", `told the owner once that ${name} has not moved to the HQ Fleet Runtime (${decision.reason})`);
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
log("warn", `could not tell the owner why ${name} has not moved to the HQ Fleet Runtime (${err instanceof Error ? err.message : String(err)}); the next start tries again`);
|
|
794
|
+
}
|
|
664
795
|
}
|
|
665
796
|
/** Remove a legacy bot's own startup agent once the bot runs on the HQ Fleet Runtime. True when there was one. */
|
|
666
797
|
function retireLegacyBotAgent(name, dir, config, bootout) {
|
|
@@ -1576,7 +1707,7 @@ async function runBotResident(nameArg) {
|
|
|
1576
1707
|
console.log(orphanBotRunMessage(name, outcome));
|
|
1577
1708
|
return;
|
|
1578
1709
|
}
|
|
1579
|
-
if (await convertLegacyBotToFleet(name, dir, config, "run"))
|
|
1710
|
+
if ((await convertLegacyBotToFleet(name, dir, config, "run")).fleet)
|
|
1580
1711
|
return;
|
|
1581
1712
|
if (healNodePathFromJob(name, dir, config))
|
|
1582
1713
|
return;
|
|
@@ -1694,18 +1825,28 @@ async function runBotList(opts) {
|
|
|
1694
1825
|
async function runBotStatus(nameArg, opts) {
|
|
1695
1826
|
const { name, dir, config } = requireBot(nameArg);
|
|
1696
1827
|
const fleet = isFleetRuntimeBot(config);
|
|
1697
|
-
const
|
|
1828
|
+
const legacy = fleet ? null : legacyMoveState(dir, config, legacyConvertSeams.providerLogin(dir, config));
|
|
1829
|
+
const row = rowFor(name, { legacy });
|
|
1698
1830
|
await decorateOnline([row]);
|
|
1699
1831
|
const status = readBotStatus(dir);
|
|
1700
1832
|
const creds = readBotCredsIdentity(dir);
|
|
1701
1833
|
const channels = fleet ? fleetLocalChannels(readFleetLocalGatewayState(config.agentUid), row.processAlive) : null;
|
|
1702
1834
|
if (opts.json) {
|
|
1703
|
-
console.log(JSON.stringify({
|
|
1835
|
+
console.log(JSON.stringify({
|
|
1836
|
+
...row,
|
|
1837
|
+
executive: executiveStatus(config, dir),
|
|
1838
|
+
...(fleet ? { engine: config.engine, channels } : {}),
|
|
1839
|
+
credsPresent: fleet || !!creds,
|
|
1840
|
+
login: describeBotAuth(dir, config),
|
|
1841
|
+
status,
|
|
1842
|
+
}, null, 2));
|
|
1704
1843
|
return;
|
|
1705
1844
|
}
|
|
1706
1845
|
console.log(`${chalk.bold(name)} ${row.agentUid}`);
|
|
1707
1846
|
if (fleet)
|
|
1708
1847
|
console.log(` engine: HQ Fleet Runtime ${config.engine?.version ?? ""} ${chalk.dim(`logs ~/.hq-fleet/profiles/${config.agentUid}/logs`)}`);
|
|
1848
|
+
else
|
|
1849
|
+
console.log(` engine: ${chalk.yellow(`legacy ${runtimeDisplayName(config.runtime)} runner (not on the HQ Fleet Runtime)`)}`);
|
|
1709
1850
|
console.log(` kind: ${describeKind(row.kind, row.companies)}`);
|
|
1710
1851
|
const login = describeBotAuth(dir, config);
|
|
1711
1852
|
console.log(` runtime: ${row.runtime} model ${row.model ?? "default"} thinking ${row.effort}${row.effortIsDefault ? " (default)" : ""}`);
|
|
@@ -1732,6 +1873,10 @@ async function runBotStatus(nameArg, opts) {
|
|
|
1732
1873
|
else {
|
|
1733
1874
|
console.log(` launchd: ${row.daemonInstalled ? (row.daemonLoaded ? "installed, loaded" : "installed, not loaded") : "not installed"}`);
|
|
1734
1875
|
}
|
|
1876
|
+
if (legacy) {
|
|
1877
|
+
for (const line of legacyStatusLines(name, legacy))
|
|
1878
|
+
console.log(` ${line}`);
|
|
1879
|
+
}
|
|
1735
1880
|
if (row.daemonNodeMissing) {
|
|
1736
1881
|
console.log(` ${chalk.red(`node missing: ${row.daemonNodeMissing}`)} — launchd cannot start ${name}; fix with: hq bot start ${name}`);
|
|
1737
1882
|
}
|
|
@@ -1781,6 +1926,37 @@ async function runBotStatus(nameArg, opts) {
|
|
|
1781
1926
|
}
|
|
1782
1927
|
console.log(chalk.dim(` state dir: ${dir}`));
|
|
1783
1928
|
}
|
|
1929
|
+
/** The `legacy` object in `hq bot status --json` for a bot still on its legacy runner. */
|
|
1930
|
+
export function legacyStatusJson(state) {
|
|
1931
|
+
const { decision } = state;
|
|
1932
|
+
return {
|
|
1933
|
+
moves: decision.convert,
|
|
1934
|
+
...(decision.convert ? {} : { reason: decision.reason, detail: decision.detail, cause: state.notice?.cause ?? null, fix: state.notice?.fix ?? null }),
|
|
1935
|
+
lastMoveFailure: state.lastFailure,
|
|
1936
|
+
ownerNotifiedAt: state.hold?.notifiedAt ?? null,
|
|
1937
|
+
reportId: state.hold?.reportId ?? null,
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
/** The `fleet move:` lines of `hq bot status` for a bot still on its legacy runner. */
|
|
1941
|
+
export function legacyStatusLines(name, state) {
|
|
1942
|
+
const lines = [];
|
|
1943
|
+
const { decision } = state;
|
|
1944
|
+
if (decision.convert) {
|
|
1945
|
+
lines.push(`fleet move: ready; it moves to the HQ Fleet Runtime at its next start (hq bot start ${name})`);
|
|
1946
|
+
}
|
|
1947
|
+
else {
|
|
1948
|
+
lines.push(`fleet move: ${chalk.yellow(`held (${decision.reason})`)}: ${state.notice?.cause ?? decision.detail}`);
|
|
1949
|
+
if (state.notice?.fix)
|
|
1950
|
+
lines.push(` fix: ${state.notice.fix}`);
|
|
1951
|
+
if (legacyHoldNotifiesOwner(decision.reason))
|
|
1952
|
+
lines.push(chalk.dim(` owner DM: ${state.hold?.notifiedAt ? `sent ${state.hold.notifiedAt}` : "not sent yet"}`));
|
|
1953
|
+
}
|
|
1954
|
+
if (state.lastFailure) {
|
|
1955
|
+
const f = state.lastFailure;
|
|
1956
|
+
lines.push(`last move: ${chalk.yellow(`failed ${f.at}${f.step ? ` (could not ${f.step})` : ""}${f.error ? `: ${f.error}` : ""}`)}; it tries again at its next start`);
|
|
1957
|
+
}
|
|
1958
|
+
return lines;
|
|
1959
|
+
}
|
|
1784
1960
|
async function runBotLogs(nameArg, opts) {
|
|
1785
1961
|
const { dir } = requireBot(nameArg);
|
|
1786
1962
|
const file = botLogPath(dir);
|
|
@@ -2503,6 +2679,7 @@ async function runBotAuth(nameArg, opts) {
|
|
|
2503
2679
|
if (legacy && engine) {
|
|
2504
2680
|
const moved = { ...(readBotConfig(b.dir) ?? legacy), engine };
|
|
2505
2681
|
writeBotConfig(b.dir, moved);
|
|
2682
|
+
clearFleetMoveRecords(b.dir);
|
|
2506
2683
|
fs.rmSync(botCredsPath(b.dir), { force: true });
|
|
2507
2684
|
createBotLogger(b.dir)("info", `moved ${b.name} to the HQ Fleet Runtime with its ${opts.provider} sign-in; its ${legacy.runtime} runner is retired`);
|
|
2508
2685
|
syncFleetRuntimeJobs(b.name, b.dir, moved);
|
|
@@ -2510,8 +2687,11 @@ async function runBotAuth(nameArg, opts) {
|
|
|
2510
2687
|
}
|
|
2511
2688
|
const signedInRuntime = result.runtimeProvider === "anthropic" ? "claude" : result.runtimeProvider === "xai-oauth" ? "grok" : result.runtimeProvider === "openai-codex" ? "codex" : null;
|
|
2512
2689
|
const pending = b.config.fleetAuthPending === true || legacy !== null;
|
|
2690
|
+
// A moved bot answers with the login it just signed in with; its legacy own-login setting no longer applies.
|
|
2691
|
+
const runtimeAuth = legacy && signedInRuntime && legacy.runtimeAuth?.[signedInRuntime] !== undefined ? withRuntimeAuth(legacy.runtimeAuth, signedInRuntime, null) : undefined;
|
|
2513
2692
|
patchBotConfig(b.dir, {
|
|
2514
2693
|
...(signedInRuntime ? { runtime: signedInRuntime } : {}),
|
|
2694
|
+
...(legacy && signedInRuntime && legacy.runtimeAuth?.[signedInRuntime] !== undefined ? { runtimeAuth } : {}),
|
|
2515
2695
|
model: result.model,
|
|
2516
2696
|
...(pending ? { fleetAuthPending: false, fleetServiceInstallPending: !result.restarted, enabled: result.restarted } : {}),
|
|
2517
2697
|
});
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -59,10 +59,49 @@ export declare class SyncSessionExpiredError extends Error {
|
|
|
59
59
|
readonly reason?: string | undefined;
|
|
60
60
|
constructor(kind: "reauth" | "unavailable", reason?: string | undefined);
|
|
61
61
|
}
|
|
62
|
+
/** Shown when the vault rejected the session even after a forced refresh. */
|
|
63
|
+
export declare const SYNC_SESSION_REJECTED_MESSAGE = "The HQ server rejected your session even after it was refreshed, so the saved sign-in is no longer accepted. `hq auth status` only checks the saved expiry, not the server. Run `hq login`, then rerun the sync.";
|
|
64
|
+
/** Reason text when token acquisition does not settle within the bound. */
|
|
65
|
+
export declare const SYNC_TOKEN_TIMEOUT_REASON = "timed out waiting for the sign-in service";
|
|
66
|
+
/** Longest a single token acquisition or refresh may take before the sync fails. */
|
|
67
|
+
export declare const SYNC_TOKEN_TIMEOUT_MS = 45000;
|
|
68
|
+
/**
|
|
69
|
+
* The vault rejected the session after a forced refresh had already replaced
|
|
70
|
+
* the saved token. Retrying again cannot help; the user has to sign in again.
|
|
71
|
+
*/
|
|
72
|
+
export declare class SyncSessionRejectedError extends Error {
|
|
73
|
+
constructor();
|
|
74
|
+
}
|
|
62
75
|
export interface SyncVaultConfigDeps {
|
|
63
76
|
/** Non-interactive token resolver: returns the cached token or refreshes it. */
|
|
64
77
|
ensureToken?: () => Promise<string>;
|
|
78
|
+
/** Bound on one token acquisition (default {@link SYNC_TOKEN_TIMEOUT_MS}). */
|
|
79
|
+
tokenTimeoutMs?: number;
|
|
65
80
|
}
|
|
81
|
+
export interface RejectedSessionRecoveryDeps {
|
|
82
|
+
/** Refresh the saved session even though it looks valid. */
|
|
83
|
+
forceRefresh?: () => Promise<{
|
|
84
|
+
refreshed: boolean;
|
|
85
|
+
reason?: string;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* The token-getter failure recorded for the latest attempt, if any. hq-cloud
|
|
89
|
+
* turns a getter error (network, refresh failure, identity swap) into a plain
|
|
90
|
+
* 401, so a 401 with a recorded failure is not a server rejection of a token.
|
|
91
|
+
*/
|
|
92
|
+
recordedFailure?: () => Error | null;
|
|
93
|
+
/** Runs once, after a recoverable 401 and before the refresh and rerun. */
|
|
94
|
+
onRetry?: () => void | Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* feedback_3d752910: the vault can reject a token that still looks valid in the
|
|
98
|
+
* local cache (revoked or replaced server-side). The cached expiry check never
|
|
99
|
+
* notices, and `hq auth refresh` is a no-op for a valid-looking token, so every
|
|
100
|
+
* sync kept presenting the same rejected token. On a vault 401, force one real
|
|
101
|
+
* refresh and rerun the sync once. A second rejection is reported as a rejected
|
|
102
|
+
* session (sign in again), never retried again.
|
|
103
|
+
*/
|
|
104
|
+
export declare function runWithRejectedSessionRecovery<T>(run: () => Promise<T>, deps?: RejectedSessionRecoveryDeps): Promise<T>;
|
|
66
105
|
/** The last token failure recorded by a {@link buildSyncVaultConfig} config. */
|
|
67
106
|
export declare function syncAuthFailureFor(config: VaultServiceConfig | undefined): Error | null;
|
|
68
107
|
/**
|