@indigoai-us/hq-cli 5.345.174 → 5.345.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/command-catalog.generated.d.ts +65 -0
  3. package/dist/command-catalog.generated.js +80 -0
  4. package/dist/commands/bot.d.ts +28 -2
  5. package/dist/commands/bot.js +32 -6
  6. package/dist/commands/daemon.js +16 -0
  7. package/dist/commands/dm.d.ts +7 -0
  8. package/dist/commands/dm.js +16 -4
  9. package/dist/commands/lanes-loop.d.ts +4 -1
  10. package/dist/commands/lanes-loop.js +23 -4
  11. package/dist/commands/lanes-shared.d.ts +5 -0
  12. package/dist/commands/lanes.d.ts +5 -7
  13. package/dist/commands/lanes.js +65 -5
  14. package/dist/commands/sync-core.d.ts +17 -0
  15. package/dist/commands/sync-core.js +46 -5
  16. package/dist/commands/sync-manifest.d.ts +6 -0
  17. package/dist/commands/sync-manifest.js +13 -0
  18. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +448 -226
  19. package/dist/hq-cloud-sync-runner/package.json +1 -1
  20. package/dist/lib/bot/cli-update.d.ts +36 -0
  21. package/dist/lib/bot/cli-update.js +160 -42
  22. package/dist/lib/bot/run.d.ts +23 -0
  23. package/dist/lib/bot/run.js +174 -23
  24. package/dist/lib/bot/runtime/claude.d.ts +7 -1
  25. package/dist/lib/bot/runtime/claude.js +29 -0
  26. package/dist/lib/bot/runtime/codex.d.ts +5 -0
  27. package/dist/lib/bot/runtime/codex.js +13 -0
  28. package/dist/lib/bot/runtime/index.js +18 -2
  29. package/dist/lib/bot/runtime/types.d.ts +23 -0
  30. package/dist/lib/bot/runtime/types.js +13 -0
  31. package/dist/lib/bot/runtime-failure.d.ts +17 -6
  32. package/dist/lib/bot/runtime-failure.js +54 -9
  33. package/dist/lib/bot/runtime-sign-in-check.d.ts +32 -0
  34. package/dist/lib/bot/runtime-sign-in-check.js +53 -0
  35. package/dist/lib/bot/runtime-sign-in.d.ts +32 -3
  36. package/dist/lib/bot/runtime-sign-in.js +17 -2
  37. package/dist/lib/bot/status.d.ts +35 -1
  38. package/dist/lib/bot/status.js +28 -0
  39. package/dist/lib/daemon/catalog.d.ts +4 -0
  40. package/dist/lib/daemon/catalog.js +47 -19
  41. package/dist/lib/daemon/run.js +1 -0
  42. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  43. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  44. package/dist/lib/lanes/loop-disposition.d.ts +44 -0
  45. package/dist/lib/lanes/loop-disposition.js +302 -0
  46. package/dist/lib/lanes/loop.d.ts +16 -3
  47. package/dist/lib/lanes/loop.js +167 -60
  48. package/dist/lib/lanes/reconcile.d.ts +5 -0
  49. package/dist/lib/lanes/reconcile.js +79 -4
  50. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +3 -0
  51. package/dist/lib/mesh/live/daemon/work-subscribe.js +41 -12
  52. package/dist/lib/sync-core/engine-home.d.ts +11 -4
  53. package/dist/lib/sync-core/engine-home.js +22 -21
  54. package/dist/lib/sync-core/manifest-scopes.d.ts +15 -0
  55. package/dist/lib/sync-core/manifest-scopes.js +26 -0
  56. package/dist/lib/sync-core/reconcile.d.ts +2 -2
  57. package/dist/lib/sync-core/reconcile.js +70 -46
  58. package/dist/lib/sync-core/release.d.ts +2 -2
  59. package/dist/lib/sync-core/release.js +31 -6
  60. package/dist/lib/sync-core/rollout-state.d.ts +6 -3
  61. package/dist/lib/sync-core/rollout-state.js +26 -22
  62. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.176] — 2026-10-28
6
+ - `hq bot list --json` rows carry `runtimeCrash`. It is set when the bot's coding tool crashed before a turn started, the crash came after the bot last started, and no turn has worked since; otherwise it is null. It gives the time, exit code, how long the tool ran, and the first line of what the tool printed. A restart or a working turn clears it. HQ uses it to warn before sending the bot work it would fail, for example the Daily Brief's Create brief.
7
+ - A local bot started by the HQ app on macOS now restarts itself, between turns, when an HQ app update replaces the app's launcher. Before this, macOS stopped letting the bot's coding tool read the Documents folder after the update, and every message failed within milliseconds ("Claude Code stopped with an error") until the bot was restarted by hand.
8
+ - A local bot now checks its own coding tool's sign-in before each turn, at start, and every 15 seconds while it is signed out, in the bot's own environment (`claude auth status --json`, `codex login status`). A bot started by launchd has a bare environment, so Claude Code could be signed in for the terminal and signed out for the bot; the turn then died in milliseconds with a bare "unknown error" that read as a crash. The bot now holds the message, says once that the tool is not signed in on this computer (with the Sign in card), and answers as soon as the sign-in works. `hq bot status --json` and `hq bot list --json` report `runtimeSignedIn` (true, false, or null when the bot cannot say), `runtimeSignInCheck` and `signInCommand`, so the HQ app can check before it sends a Daily Brief job. Turns also carry `USER` (and `LOGNAME`) from the account the bot runs as when the environment lacks them, since Claude Code keys its Keychain login on `$USER`.
9
+ - On any machine where `hq daemon` runs the new sync engine (`sync-core`), it now also runs
10
+ the `mesh` service, which writes the engine's wake marker. Without it, a machine
11
+ that turned on `sync-core` with an override (for example an agent kit) had no remote
12
+ wakes and fell back to polling every 1 to 30 minutes. Disabling `mesh` explicitly still
13
+ turns it off.
14
+ - Recover durable unfinished loop queues after controller loss through normal admission. Hold uncertain effects for explicit senior, local-host and company-bound `hq lanes recover-loop --lane-version` disposition with an audited fingerprint; never replay uncertain work or reactivate historical done lanes. Preserve operator stops, questions, account eligibility and live ownership, reject linked recovery directories, and serialize controller starts with recovery. Legacy ownerless lease publication and old-client reclaimer races remain separately gated.
15
+ - The new sync engine now installs as release v0.1.19, and company vaults move to full mode only on v0.1.19 or later. On a busy company vault, a remote change now costs one request instead of about four, and an idle engine no longer runs extra follow-up checks.
16
+ - The new sync engine now installs as release v0.1.20, and company vaults move to full mode only on v0.1.20 or later. Remote changes now arrive within seconds instead of after a 30 second hold, and Linux now picks up local changes as they happen instead of waiting for the next poll. A file deleted right after the engine uploaded it is now deleted on the server instead of coming back, editing a file before its upload shows on the server no longer creates a conflict copy, and two-sided board.json edits merge by entry id instead of conflicting. Wakes that change nothing locally no longer send requests.
17
+ - When a company's `sync.rust-engine-rollout` switch is on, the daemon now moves that company's vault and the member's personal vault straight to the new sync engine in full mode on its next reconcile: the legacy sync is turned off for both vaults and the engine starts once the legacy runner has stopped syncing them. The read-only warming phase and the plan-size promotion hold are gone. Company vaults run full from engine 0.1.17 on, the engine the daemon now installs; a machine left with an older engine keeps its company vaults on the current sync and lists them under `held` in `hq sync-core status`. The engine's first full cycle with work (planned, completed, pending, held, not applied, and the uploads, downloads, deletes, conflict copies and deferred paths in its work list) is recorded per vault in `~/.hq/sync-core/rollout.json`. Turning the switch off, or naming a vault in `~/.hq/sync-core/skip.json`, stops the engine for that vault, removes its ownership record and turns the legacy sync back on for it, including a vault that was already off before the switch and when `rollout.json` cannot be read.
18
+ - The daemon decides whether an installed sync engine is new enough from the version the binary itself reports, falling back to the `hq-syncd.release` marker only when the binary cannot be run, and corrects a stale marker, so a marker left at an older release beside a newer binary no longer makes the daemon replace that binary with an older one. A read-only engine left from an earlier release runs without the protocol variable and heartbeat watchdog until the next reconcile moves it to full mode.
19
+ - The daemon's `sync-core` service now uploads a sync-audit manifest once a day for every vault the new sync engine owns on this machine: a vault the daemon runs in full mode whose current sync is switched off here. A `sync-core-manifest` unit runs `hq daemon service sync-core-manifest`, which calls `hq sync manifest --engine-owned --respect-throttle` per scope. A vault whose sync is merely switched off, or whose current sync is still on, gets no engine-owned manifest, so two uploaders never share a scope. Without this the nightly reconciliation audit marks engine-owned scopes `manifest_stale` from the second day, because only the current sync uploaded manifests. `hq sync manifest --engine-owned` builds the manifest from the disk walk alone and marks the legacy journal as not the scope's ledger (`ledgerUnavailable.errorClass` = `SyncCoreOwnsScope`), so the audit compares disk against the vault instead of reading the frozen journal's upload claims as differences.
20
+ - A Mac whose daemon runs the current sync no longer waits ten minutes before starting the new sync engine for a vault just because the HQ desktop app is open. The ten-minute wait now applies only when the desktop app runs the current sync itself; when the daemon runs it, the engine starts as soon as the daemon's sync runner has restarted without that vault.
21
+ - The daemon's push-wake marker `~/.hq/work-mesh/sync-wake.json` now carries a
22
+ per-vault `vaults` map (`{ "<vault uid>": { "wakeSequence": n } }`) next to the
23
+ shared `wakeSequence`, read from the IoT envelope's `vault` field, so each
24
+ hq-sync-core engine wakes only for its own vault. Old markers are read and
25
+ carried forward.
26
+
27
+ ## [5.345.175] — 2026-10-28
28
+ - A local bot whose coding tool crashed before it could start on a message (seen as "Claude Code stopped with an error" on the first message after a day or more idle) now tries that message once more on its own. If it crashes again, the reply carries a Heal bot card so HQ can restart the bot and send the message again, and the bot's log and `hq bot status` record what the tool printed, how long it ran and which binary it was, so the cause can be found.
29
+ - `hq dm --attach` after a person `hq login` looks up the caller's `prs_` uid
30
+ when the session token has no `custom:entityUid` (human tokens never carry
31
+ that claim). Plain `hq dm` already worked; attaching a file no longer fails
32
+ with "Could not read your person uid from the session token".
33
+
5
34
  ## [5.345.174] — 2026-10-27
6
35
  - `hq channels delete <name> --yes` deletes a channel you own, including its
7
36
  messages. Group DMs stay undeletable. Without `--yes` the command refuses.
@@ -1180,6 +1180,10 @@ export declare const COMMAND_CATALOG: readonly [{
1180
1180
  readonly flags: "--respect-throttle";
1181
1181
  readonly description: "Honour the 24h per-scope upload throttle instead of forcing a pass (for scheduled callers)";
1182
1182
  readonly hidden: false;
1183
+ }, {
1184
+ readonly flags: "--engine-owned";
1185
+ readonly description: "The new sync engine owns this scope here: build the manifest from disk alone and mark the legacy journal as not its ledger";
1186
+ readonly hidden: false;
1183
1187
  }, {
1184
1188
  readonly flags: "--hq-root <path>";
1185
1189
  readonly description: "Local HQ tree root (default: <runtime-path>)";
@@ -15695,6 +15699,47 @@ export declare const COMMAND_CATALOG: readonly [{
15695
15699
  readonly hidden: false;
15696
15700
  }];
15697
15701
  readonly subcommands: readonly [];
15702
+ }, {
15703
+ readonly name: "recover-loop";
15704
+ readonly description: "Preview or explicitly fail/abandon uncertain loop effects; never replay work";
15705
+ readonly aliases: readonly [];
15706
+ readonly hidden: false;
15707
+ readonly helpOption: true;
15708
+ readonly helpCommand: false;
15709
+ readonly usage: "[options] <lane-id>";
15710
+ readonly helpText: readonly [];
15711
+ readonly arguments: readonly [{
15712
+ readonly name: "lane-id";
15713
+ readonly required: true;
15714
+ readonly variadic: false;
15715
+ readonly description: "Loop lane id";
15716
+ }];
15717
+ readonly options: readonly [{
15718
+ readonly flags: "--disposition <action>";
15719
+ readonly description: "fail or abandon; omit for a read-only preview";
15720
+ readonly hidden: false;
15721
+ }, {
15722
+ readonly flags: "--lane-version <number>";
15723
+ readonly description: "Exact lane version from preview";
15724
+ readonly hidden: false;
15725
+ }, {
15726
+ readonly flags: "--fingerprint <sha256>";
15727
+ readonly description: "Exact preview envelope/lease fingerprint";
15728
+ readonly hidden: false;
15729
+ }, {
15730
+ readonly flags: "--reason <text>";
15731
+ readonly description: "Audited reason; does not certify external effects";
15732
+ readonly hidden: false;
15733
+ }, {
15734
+ readonly flags: "--attest-owner-investigated";
15735
+ readonly description: "Attest unknown lease ownership was investigated and no live owner exists";
15736
+ readonly hidden: false;
15737
+ }, {
15738
+ readonly flags: "--json";
15739
+ readonly description: "JSON on stdout (always)";
15740
+ readonly hidden: false;
15741
+ }];
15742
+ readonly subcommands: readonly [];
15698
15743
  }, {
15699
15744
  readonly name: "enqueue";
15700
15745
  readonly description: "Validate and queue one phase envelope for a loop lane";
@@ -18382,6 +18427,26 @@ export declare const COMMAND_CATALOG: readonly [{
18382
18427
  readonly hidden: false;
18383
18428
  }];
18384
18429
  readonly subcommands: readonly [];
18430
+ }, {
18431
+ readonly name: "sync-core-manifest";
18432
+ readonly description: "Upload the sync-audit manifest for every vault the new sync engine owns on this machine";
18433
+ readonly aliases: readonly [];
18434
+ readonly hidden: false;
18435
+ readonly helpOption: true;
18436
+ readonly helpCommand: false;
18437
+ readonly usage: "[options]";
18438
+ readonly helpText: readonly [];
18439
+ readonly arguments: readonly [];
18440
+ readonly options: readonly [{
18441
+ readonly flags: "--hq-root <path>";
18442
+ readonly description: "HQ folder the engine works against";
18443
+ readonly hidden: false;
18444
+ }, {
18445
+ readonly flags: "--json";
18446
+ readonly description: "Print one JSON array of per-scope outcomes";
18447
+ readonly hidden: false;
18448
+ }];
18449
+ readonly subcommands: readonly [];
18385
18450
  }, {
18386
18451
  readonly name: "remote-control";
18387
18452
  readonly description: "Keep `claude remote-control` running in a tmux session in the HQ folder";
@@ -1419,6 +1419,11 @@ export const COMMAND_CATALOG = [
1419
1419
  "description": "Honour the 24h per-scope upload throttle instead of forcing a pass (for scheduled callers)",
1420
1420
  "hidden": false
1421
1421
  },
1422
+ {
1423
+ "flags": "--engine-owned",
1424
+ "description": "The new sync engine owns this scope here: build the manifest from disk alone and mark the legacy journal as not its ledger",
1425
+ "hidden": false
1426
+ },
1422
1427
  {
1423
1428
  "flags": "--hq-root <path>",
1424
1429
  "description": "Local HQ tree root (default: <runtime-path>)",
@@ -19117,6 +19122,57 @@ export const COMMAND_CATALOG = [
19117
19122
  ],
19118
19123
  "subcommands": []
19119
19124
  },
19125
+ {
19126
+ "name": "recover-loop",
19127
+ "description": "Preview or explicitly fail/abandon uncertain loop effects; never replay work",
19128
+ "aliases": [],
19129
+ "hidden": false,
19130
+ "helpOption": true,
19131
+ "helpCommand": false,
19132
+ "usage": "[options] <lane-id>",
19133
+ "helpText": [],
19134
+ "arguments": [
19135
+ {
19136
+ "name": "lane-id",
19137
+ "required": true,
19138
+ "variadic": false,
19139
+ "description": "Loop lane id"
19140
+ }
19141
+ ],
19142
+ "options": [
19143
+ {
19144
+ "flags": "--disposition <action>",
19145
+ "description": "fail or abandon; omit for a read-only preview",
19146
+ "hidden": false
19147
+ },
19148
+ {
19149
+ "flags": "--lane-version <number>",
19150
+ "description": "Exact lane version from preview",
19151
+ "hidden": false
19152
+ },
19153
+ {
19154
+ "flags": "--fingerprint <sha256>",
19155
+ "description": "Exact preview envelope/lease fingerprint",
19156
+ "hidden": false
19157
+ },
19158
+ {
19159
+ "flags": "--reason <text>",
19160
+ "description": "Audited reason; does not certify external effects",
19161
+ "hidden": false
19162
+ },
19163
+ {
19164
+ "flags": "--attest-owner-investigated",
19165
+ "description": "Attest unknown lease ownership was investigated and no live owner exists",
19166
+ "hidden": false
19167
+ },
19168
+ {
19169
+ "flags": "--json",
19170
+ "description": "JSON on stdout (always)",
19171
+ "hidden": false
19172
+ }
19173
+ ],
19174
+ "subcommands": []
19175
+ },
19120
19176
  {
19121
19177
  "name": "enqueue",
19122
19178
  "description": "Validate and queue one phase envelope for a loop lane",
@@ -22399,6 +22455,30 @@ export const COMMAND_CATALOG = [
22399
22455
  ],
22400
22456
  "subcommands": []
22401
22457
  },
22458
+ {
22459
+ "name": "sync-core-manifest",
22460
+ "description": "Upload the sync-audit manifest for every vault the new sync engine owns on this machine",
22461
+ "aliases": [],
22462
+ "hidden": false,
22463
+ "helpOption": true,
22464
+ "helpCommand": false,
22465
+ "usage": "[options]",
22466
+ "helpText": [],
22467
+ "arguments": [],
22468
+ "options": [
22469
+ {
22470
+ "flags": "--hq-root <path>",
22471
+ "description": "HQ folder the engine works against",
22472
+ "hidden": false
22473
+ },
22474
+ {
22475
+ "flags": "--json",
22476
+ "description": "Print one JSON array of per-scope outcomes",
22477
+ "hidden": false
22478
+ }
22479
+ ],
22480
+ "subcommands": []
22481
+ },
22402
22482
  {
22403
22483
  "name": "remote-control",
22404
22484
  "description": "Keep `claude remote-control` running in a tmux session in the HQ folder",
@@ -1,7 +1,8 @@
1
1
  import { Command } from "commander";
2
- import { type RuntimeSignInIssue } from "../lib/bot/runtime-sign-in.js";
2
+ import { type RuntimeSignInCheck, type RuntimeSignInIssue } from "../lib/bot/runtime-sign-in.js";
3
+ import type { BotStatusFile } from "../lib/bot/status.js";
3
4
  import { probeBotRuntime, type RuntimeProbeResult } from "../lib/bot/probe.js";
4
- import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type BotConfig, type BotRuntimeId, type LaunchctlExec, type BotRole } from "../lib/bot/index.js";
5
+ import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type RuntimeCrash, type BotConfig, type BotRuntimeId, type LaunchctlExec, type BotRole } from "../lib/bot/index.js";
5
6
  /** Real launchctl; every failure is a non-ok result, never a throw. */
6
7
  export declare const defaultLaunchctl: LaunchctlExec;
7
8
  /** The desktop app creates HQ's setup bot under this name, always from the `setup` worker. */
@@ -40,6 +41,25 @@ export interface BotRow {
40
41
  busySince: string | null;
41
42
  /** Present while the coding tool needs a new sign-in before the bot can work. */
42
43
  runtimeSignIn: RuntimeSignInIssue | null;
44
+ /**
45
+ * The bot's own last check of its coding tool's sign-in, in the bot's
46
+ * environment (at start, before a turn, on a timer while signed out). Null
47
+ * when the bot has not reported one (stopped, or an older hq-cli).
48
+ */
49
+ runtimeSignInCheck: RuntimeSignInCheck | null;
50
+ /**
51
+ * The bot's word on whether its coding tool is signed in for it: false
52
+ * while it holds a sign-in issue or its last check said no; true when its
53
+ * last check said yes and no issue is held; null when it cannot say.
54
+ */
55
+ runtimeSignedIn: boolean | null;
56
+ /** The terminal command that signs the bot's coding tool in on this computer. */
57
+ signInCommand: string;
58
+ /**
59
+ * The coding tool crashed before its turn started since the bot last
60
+ * started, and no turn has worked since; null otherwise. A restart clears it.
61
+ */
62
+ runtimeCrash: RuntimeCrash | null;
43
63
  daemonInstalled: boolean;
44
64
  daemonLoaded: boolean;
45
65
  /**
@@ -74,6 +94,12 @@ export interface BotRow {
74
94
  export declare function listBotNames(root: string): string[];
75
95
  /** Bot-identity token supplier: per-bot creds + per-bot token cache dir. */
76
96
  export declare function botTokenSupplier(dir: string): () => Promise<string>;
97
+ /**
98
+ * The bot's word on its coding tool's sign-in. Only a running bot's report
99
+ * counts: a stopped bot's last check may be from before a sign-in or a
100
+ * sign-out, and the next start checks afresh.
101
+ */
102
+ export declare function runtimeSignedIn(status: Pick<BotStatusFile, "runtimeSignIn" | "runtimeSignInCheck"> | null | undefined, processAlive: boolean): boolean | null;
77
103
  /** Spawn `hq bot run <name>` detached (no launchd). */
78
104
  export declare function spawnDetachedRun(name: string, dir: string): number;
79
105
  /**
@@ -30,7 +30,7 @@ import { DecisionsClient } from "../lib/decisions/client.js";
30
30
  import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
31
31
  import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, runResidentCommandWithPackageLease, } from "../utils/resident-package-preload.js";
32
32
  import { followLogFile } from "../lib/log-follow.js";
33
- import { CLI_UPDATE_DISABLE_ENV, createCliUpdateWatcher, handOffToUpdatedCli } from "../lib/bot/cli-update.js";
33
+ import { CLI_UPDATE_DISABLE_ENV, createCliUpdateWatcher, detectHqAppLauncher, handOffToUpdatedCli } from "../lib/bot/cli-update.js";
34
34
  import { createVaultObjectDownloader } from "../lib/agent-kit/attachment-download.js";
35
35
  import { addBotToCompanies, describeCompanyFailures, removeBotFromCompanies } from "./bot-companies.js";
36
36
  import { registerBotContinuityCommand } from "./bot-continuity.js";
@@ -49,10 +49,10 @@ import { askDaemonToStart, askDaemonToStop, daemonOwnsProcess, daemonHoldReason,
49
49
  import { daemonPaths } from "../lib/daemon/paths.js";
50
50
  import { launchdLabel } from "../lib/bot/paths.js";
51
51
  import { isAncestorProcess } from "../lib/bot/self-process.js";
52
- import { runtimeDisplayName } from "../lib/bot/runtime-sign-in.js";
52
+ import { runtimeDisplayName, signInCommand } from "../lib/bot/runtime-sign-in.js";
53
53
  import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
54
54
  import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
55
- 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, 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, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
55
+ 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, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
56
56
  function collectRepeated(value, previous) {
57
57
  return [...previous, value];
58
58
  }
@@ -176,6 +176,18 @@ async function ownerToken(interactive = process.stdin.isTTY === true) {
176
176
  // sign-in and shares the same cached session file.
177
177
  return ensureCognitoToken({ tokenSource: "person", interactive });
178
178
  }
179
+ /**
180
+ * The bot's word on its coding tool's sign-in. Only a running bot's report
181
+ * counts: a stopped bot's last check may be from before a sign-in or a
182
+ * sign-out, and the next start checks afresh.
183
+ */
184
+ export function runtimeSignedIn(status, processAlive) {
185
+ if (!processAlive || !status)
186
+ return null;
187
+ if (status.runtimeSignIn)
188
+ return false;
189
+ return status.runtimeSignInCheck?.signedIn ?? null;
190
+ }
179
191
  function rowFor(name, opts = {}) {
180
192
  const dir = botDir(name);
181
193
  const config = readBotConfig(dir);
@@ -221,6 +233,10 @@ function rowFor(name, opts = {}) {
221
233
  busy: processAlive && inflightTurns.length > 0,
222
234
  busySince: processAlive ? (inflightTurns[0]?.startedAt ?? null) : null,
223
235
  runtimeSignIn: status?.runtimeSignIn ?? null,
236
+ runtimeSignInCheck: status?.runtimeSignInCheck ?? null,
237
+ runtimeSignedIn: runtimeSignedIn(status, processAlive),
238
+ signInCommand: signInCommand(config.runtime),
239
+ runtimeCrash: processAlive ? runtimeCrashSinceStart(status) : null,
224
240
  daemonInstalled,
225
241
  daemonLoaded,
226
242
  daemonNodeMissing,
@@ -1158,9 +1174,11 @@ async function runBotResident(nameArg) {
1158
1174
  downloadAttachment: createVaultObjectDownloader({ getToken: token }),
1159
1175
  // Reload onto an updated hq-cli: the watched path is the script this
1160
1176
  // process was launched with, which is what its supervisor launches next.
1177
+ // Also restart when the HQ.app launcher is replaced: macOS ties the bot's
1178
+ // Documents access to that binary, and coding-tool turns fail until restart.
1161
1179
  ...(process.env[CLI_UPDATE_DISABLE_ENV] === "1"
1162
1180
  ? {}
1163
- : { cliUpdate: createCliUpdateWatcher({ dir, entry: process.argv[1] ?? null }) }),
1181
+ : { cliUpdate: createCliUpdateWatcher({ dir, entry: process.argv[1] ?? null, launcher: detectHqAppLauncher() }) }),
1164
1182
  onCliUpdateRestart: (_update, log) => {
1165
1183
  handOffToUpdatedCli(name, log, { spawnReplacement: () => spawnDetachedRun(name, dir) });
1166
1184
  },
@@ -1250,13 +1268,21 @@ async function runBotStatus(nameArg, opts) {
1250
1268
  console.log(` started: ${status.startedAt} restarts: ${status.restarts} replies: ${status.repliesSent}`);
1251
1269
  if (status.cliVersion)
1252
1270
  console.log(` hq-cli: ${status.cliVersion}`);
1253
- if (status.runtimeSignIn?.state === "expired") {
1271
+ if (status.runtimeSignIn) {
1254
1272
  const tool = runtimeDisplayName(status.runtimeSignIn.runtime);
1255
- console.log(` ${chalk.yellow(`${tool} needs to sign in again`)} — sign in from HQ, or run \`${status.runtimeSignIn.runtime} ${status.runtimeSignIn.runtime === "claude" ? "auth login" : "login"}\`, then \`hq bot restart ${name}\``);
1273
+ const what = status.runtimeSignIn.state === "signed-out" ? `${tool} is not signed in for ${name}` : `${tool} needs to sign in again`;
1274
+ console.log(` ${chalk.yellow(what)} — sign in from HQ, or run \`${signInCommand(status.runtimeSignIn.runtime)}\`; ${name} checks again on its own`);
1275
+ }
1276
+ else if (status.runtimeSignInCheck) {
1277
+ const c = status.runtimeSignInCheck;
1278
+ console.log(` sign-in: ${c.signedIn === true ? chalk.green("signed in") : c.signedIn === false ? chalk.yellow("signed out") : chalk.gray("unknown")}${chalk.dim(` checked ${c.checkedAt}`)}${c.signedIn === null && c.detail ? chalk.dim(` (${c.detail})`) : ""}`);
1256
1279
  }
1257
1280
  if (status.modelHold && new Date(status.modelHold.until).getTime() > Date.now()) {
1258
1281
  console.log(` ${chalk.yellow(`holding until ${status.modelHold.until}`)} after repeated model failures; the next message after that runs again`);
1259
1282
  }
1283
+ if (status.lastCrash) {
1284
+ console.log(` last crash: ${chalk.yellow(`${status.lastCrash.at}, exit ${status.lastCrash.code ?? "?"}${status.lastCrash.durationMs !== undefined ? ` after ${status.lastCrash.durationMs} ms` : ""}`)}`);
1285
+ }
1260
1286
  if (status.runtimeModelFallback) {
1261
1287
  console.log(` model: ${chalk.yellow(`running on ${status.runtimeModelFallback.to}`)} because ${row.runtime} here is too old for ${status.runtimeModelFallback.from || "its configured model"}; update ${row.runtime} to use it`);
1262
1288
  }
@@ -1553,6 +1553,22 @@ export function registerDaemonCommand(program, over = {}) {
1553
1553
  err: d.err,
1554
1554
  });
1555
1555
  }));
1556
+ service
1557
+ .command("sync-core-manifest")
1558
+ .description("Upload the sync-audit manifest for every vault the new sync engine owns on this machine")
1559
+ .option("--hq-root <path>", "HQ folder the engine works against")
1560
+ .option("--json", "Print one JSON array of per-scope outcomes")
1561
+ .action(wrap(async (d, opts) => {
1562
+ const { runSyncCoreManifest } = await import("./sync-core.js");
1563
+ await runSyncCoreManifest(opts, {
1564
+ home: d.home,
1565
+ env: d.env,
1566
+ platform: d.platform,
1567
+ arch: process.arch,
1568
+ out: d.out,
1569
+ err: d.err,
1570
+ });
1571
+ }));
1556
1572
  service
1557
1573
  .command("remote-control")
1558
1574
  .description("Keep `claude remote-control` running in a tmux session in the HQ folder")
@@ -392,6 +392,13 @@ export interface CallerIdentity {
392
392
  * Pure → unit-testable.
393
393
  */
394
394
  export declare function callerFromToken(token: string): CallerIdentity;
395
+ /**
396
+ * Vault scope uid for `--attach`. Human Cognito sessions do not carry
397
+ * `custom:entityUid` (hq-pro sets that only on agent and outpost tokens), so
398
+ * JWT peek is not enough after a normal `hq login`. Fall back to the
399
+ * canonical person lookup used by `--personal` files and secrets.
400
+ */
401
+ export declare function callerUidForAttach(token: string): Promise<string>;
395
402
  /**
396
403
  * Newest-first channel history → the caller's most recent top-level eventId.
397
404
  * Replies (those with rootEventId) are skipped. Pure → unit-testable.
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { ensureCognitoToken } from "../utils/cognito-session.js";
4
- import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
4
+ import { resolveCallerPersonUid, resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
5
5
  import { isMachineIdentity } from "@indigoai-us/hq-cloud";
6
6
  import { peekIdToken } from "../utils/id-token.js";
7
7
  import { redactAgentInboxItem as redactInboxCredentials } from "../lib/agent-kit/inbox-redact.js";
@@ -792,9 +792,9 @@ async function runChannelSend(channelName, message, opts) {
792
792
  // personal channels in the caller's personal vault.
793
793
  let uploaded = [];
794
794
  if (localFiles.length > 0) {
795
- const callerUid = callerFromToken(token).personUid;
795
+ const callerUid = await callerUidForAttach(token);
796
796
  const scopeUid = channel.companyUid?.trim()
797
- ? attachmentScopeForChannel(channel, callerUid ?? "")
797
+ ? attachmentScopeForChannel(channel, callerUid)
798
798
  : personalScopeForCaller(callerUid);
799
799
  uploaded = await uploadDmAttachments({
800
800
  token,
@@ -989,7 +989,7 @@ async function runDmSend(recipient, message, rawOpts) {
989
989
  if (localFiles.length > 0) {
990
990
  // A 1:1 attachment is stored in the SENDER's personal vault under the
991
991
  // sorted participant pair; the server lets either member read it.
992
- const callerUid = callerFromToken(token).personUid;
992
+ const callerUid = await callerUidForAttach(token);
993
993
  const scopeUid = personalScopeForCaller(callerUid);
994
994
  const peerUid = await resolveDmPeerUid(token, recipient);
995
995
  uploaded = await uploadDmAttachments({
@@ -1363,6 +1363,18 @@ export function callerFromToken(token) {
1363
1363
  ...(emailNorm ? { email: emailNorm } : {}),
1364
1364
  };
1365
1365
  }
1366
+ /**
1367
+ * Vault scope uid for `--attach`. Human Cognito sessions do not carry
1368
+ * `custom:entityUid` (hq-pro sets that only on agent and outpost tokens), so
1369
+ * JWT peek is not enough after a normal `hq login`. Fall back to the
1370
+ * canonical person lookup used by `--personal` files and secrets.
1371
+ */
1372
+ export async function callerUidForAttach(token) {
1373
+ const fromJwt = callerFromToken(token).personUid;
1374
+ if (fromJwt)
1375
+ return fromJwt;
1376
+ return resolveCallerPersonUid(token, undefined, { command: "hq dm --attach" });
1377
+ }
1366
1378
  function isCallerMessage(m, me) {
1367
1379
  if (me.personUid && m.fromPersonUid === me.personUid)
1368
1380
  return true;
@@ -50,6 +50,9 @@ export interface LoopControllerDeps {
50
50
  export declare const LOOP_CONTROLLER_RECOVERY_REASON = "loop_controller_recovery";
51
51
  export declare const LOOP_STALL_RESTART_REASON = "loop_stall_restart";
52
52
  export declare const LOOP_PHASE_DEADLINE_REASON = "loop_phase_deadline";
53
- export declare function startLaneLoopController(ctx: Ctx, laneId: string, initialAdmission?: boolean): number;
53
+ /** Direct command starts serialize with explicit effects disposition. */
54
+ export declare function startLaneLoopController(ctx: Ctx, laneId: string, initialAdmission?: boolean): Promise<number>;
55
+ /** Internal admission dispatcher only: its caller already holds the provider lock. */
56
+ export declare function startLaneLoopControllerWithAdmissionLockHeld(ctx: Ctx, laneId: string, initialAdmission?: boolean): number;
54
57
  export declare function runLaneLoopController(ctx: Ctx, laneId: string, deps: LoopControllerDeps): Promise<void>;
55
58
  //# sourceMappingURL=lanes-loop.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { withLaneAdmissionLock } from "../lib/lanes/admission.js";
1
2
  import { spawn } from "node:child_process";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
@@ -5,10 +6,10 @@ import { readLane, retryUpdateLane, updateLane } from "../lib/lanes/store.js";
5
6
  import { LanesError, isLaneVersionConflict } from "../lib/lanes/errors.js";
6
7
  import { liveNodeBinary } from "../lib/lanes/node-binary.js";
7
8
  import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
8
- import { spawnLaneWorker } from "../lib/lanes/spawn.js";
9
+ import { readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
9
10
  import { raiseLaneQuestion } from "../lib/lanes/questions.js";
10
11
  import { isTerminalLaneState } from "../lib/lanes/types.js";
11
- import { HANDOFF_HOLD_CHOICES, clearLoopControllerPid, loopAdmissionPath, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
12
+ import { HANDOFF_HOLD_CHOICES, clearLoopControllerPid, loopAdmissionPath, loopControllerIsAlive, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
12
13
  import { laneStories } from "../lib/lanes/stories.js";
13
14
  import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
14
15
  import { LANE_LOOP_HANDOFF_REPAIR_FLAG } from "../lib/lanes/flag-keys.js";
@@ -99,7 +100,25 @@ function casLoopLaneState(hqRoot, laneId, from, to) {
99
100
  export const LOOP_CONTROLLER_RECOVERY_REASON = "loop_controller_recovery";
100
101
  export const LOOP_STALL_RESTART_REASON = "loop_stall_restart";
101
102
  export const LOOP_PHASE_DEADLINE_REASON = "loop_phase_deadline";
102
- export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
103
+ /** Direct command starts serialize with explicit effects disposition. */
104
+ export async function startLaneLoopController(ctx, laneId, initialAdmission = false) {
105
+ const root = ctx.hqRoot();
106
+ const expected = readLane(root, laneId);
107
+ return withLaneAdmissionLock(root, expected.provider, async () => {
108
+ const current = readLane(root, laneId);
109
+ if (current.version !== expected.version || current.provider !== expected.provider) {
110
+ throw new LanesError("loop_controller_start_changed", "Lane changed while controller start waited for admission; inspect again", { lane_id: laneId });
111
+ }
112
+ return startLaneLoopControllerWithAdmissionLockHeld(ctx, laneId, initialAdmission);
113
+ }, ctx.sleep);
114
+ }
115
+ /** Internal admission dispatcher only: its caller already holds the provider lock. */
116
+ export function startLaneLoopControllerWithAdmissionLockHeld(ctx, laneId, initialAdmission = false) {
117
+ const runDir = loopRunDir(ctx.hqRoot(), laneId);
118
+ const identity = ctx.controllerProcessIdentity;
119
+ if (loopControllerIsAlive(runDir, identity?.isAlive, identity?.readStarttime)) {
120
+ throw new LanesError("loop_controller_already_alive", "A loop controller or lease owner is already alive; no second controller was started", { lane_id: laneId });
121
+ }
103
122
  const entry = process.argv[1];
104
123
  if (!entry)
105
124
  throw new LanesError("loop_controller_entry_missing", "Cannot resolve the hq CLI entry point for the loop controller", { lane_id: laneId });
@@ -111,7 +130,7 @@ export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
111
130
  child.unref();
112
131
  if (child.pid === undefined)
113
132
  throw new LanesError("loop_controller_spawn_failed", "Loop controller did not start", { lane_id: laneId });
114
- setLoopControllerPid(loopRunDir(ctx.hqRoot(), laneId), laneId, child.pid);
133
+ setLoopControllerPid(loopRunDir(ctx.hqRoot(), laneId), laneId, child.pid, (identity?.readStarttime ?? readProcStarttime)(child.pid) ?? undefined);
115
134
  return child.pid;
116
135
  }
117
136
  export async function runLaneLoopController(ctx, laneId, deps) {
@@ -291,6 +291,11 @@ export interface Ctx {
291
291
  killProcess?: KillProcessFn;
292
292
  isAlive?: (pid: number) => boolean;
293
293
  readStarttime?: (pid: number) => number | null;
294
+ /** Controller identity is separate from worker liveness test seams. */
295
+ controllerProcessIdentity?: {
296
+ isAlive(pid: number): boolean;
297
+ readStarttime(pid: number): number | null;
298
+ };
294
299
  watcherLoopDeps?: Pick<WatcherLoopDeps, "sleep" | "spawnRepair">;
295
300
  stopSleepMs?: (ms: number) => void;
296
301
  livenessDeps?: LivenessDeps;
@@ -1,10 +1,3 @@
1
- /**
2
- * `hq lanes` — worker lane management and decision queue (spec §5–§7, §10–§11).
3
- *
4
- * Existing commands print JSON on stdout by default and take no interactive input, except
5
- * `accounts login`, which hands the terminal to the vendor's own sign-in.
6
- * `watch` and the monitor reminder are the line-oriented text surfaces.
7
- */
8
1
  import { Command } from "commander";
9
2
  import type { MonitorLaneMessagePayload, MonitorLaneMessageResult } from "./lanes-message.js";
10
3
  import { type Ctx, type LanesMesh, type OutpostRouteIdentity, type ResolvedResumeProfile, type ResumeProfileRepinAuthorization, type SpawnLoginResult } from "./lanes-shared.js";
@@ -77,6 +70,11 @@ export interface LanesCommandDeps {
77
70
  isAlive?: (pid: number) => boolean;
78
71
  /** Test seam for worker PID identity checks. */
79
72
  readStarttime?: (pid: number) => number | null;
73
+ /** Controller identity is separate from worker liveness test seams. */
74
+ controllerProcessIdentity?: {
75
+ isAlive(pid: number): boolean;
76
+ readStarttime(pid: number): number | null;
77
+ };
80
78
  /** Test seams for synchronous dead-worker classification and repair. */
81
79
  watcherLoopDeps?: Pick<WatcherLoopDeps, "sleep" | "spawnRepair">;
82
80
  stopSleepMs?: (ms: number) => void;