@indigoai-us/hq-cli 5.318.1-beta.0 → 5.320.0

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 (35) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/assets/scaffold/core/scripts/repo-run-registry.sh +61 -17
  3. package/assets/scaffold/core/scripts/session-journal.sh +40 -2
  4. package/dist/bin/hq-auth-refresh.d.ts +3 -1
  5. package/dist/bin/hq-auth-refresh.js +41 -6
  6. package/dist/command-catalog.generated.d.ts +22 -0
  7. package/dist/command-catalog.generated.js +29 -0
  8. package/dist/commands/cloud.js +0 -1
  9. package/dist/commands/core-hook-state-helpers-native.d.ts +1 -1
  10. package/dist/commands/core-hook-state-helpers-native.js +36 -7
  11. package/dist/commands/core-repo-run-registry-native.d.ts +1 -0
  12. package/dist/commands/core-repo-run-registry-native.js +10 -2
  13. package/dist/commands/core-rrr-worktree-scope-fix-flag.d.ts +4 -0
  14. package/dist/commands/core-rrr-worktree-scope-fix-flag.js +7 -0
  15. package/dist/commands/core-session-journal-native.js +31 -2
  16. package/dist/commands/lanes-checks.js +12 -0
  17. package/dist/commands/lanes-dropbox-repair.d.ts +14 -0
  18. package/dist/commands/lanes-dropbox-repair.js +33 -0
  19. package/dist/commands/lanes.js +44 -5
  20. package/dist/fast-core.js +6 -1
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.js +135 -63
  23. package/dist/lib/daemon/run.d.ts +2 -0
  24. package/dist/lib/daemon/run.js +10 -1
  25. package/dist/lib/lanes/autopilot.js +9 -2
  26. package/dist/lib/lanes/flag-keys.d.ts +9 -0
  27. package/dist/lib/lanes/flag-keys.js +9 -0
  28. package/dist/lib/lanes/keep-alive-audit.d.ts +1 -1
  29. package/dist/lib/lanes/keep-alive-audit.js +69 -4
  30. package/dist/lib/lanes/orphaned-dropbox.d.ts +24 -0
  31. package/dist/lib/lanes/orphaned-dropbox.js +213 -0
  32. package/dist/lib/lanes/tracker-checks.d.ts +1 -1
  33. package/dist/utils/package-use-lease.d.ts +25 -0
  34. package/dist/utils/package-use-lease.js +213 -0
  35. package/package.json +1 -1
@@ -26,6 +26,7 @@ import { registerLaneKpisCommand } from "./lanes-kpis.js";
26
26
  import { registerLaneKeepAliveAuditCommand } from "./lanes-keep-alive-audit.js";
27
27
  import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
28
28
  import { registerLaneChecksCommand } from "./lanes-checks.js";
29
+ import { registerLaneDropboxRepairCommand } from "./lanes-dropbox-repair.js";
29
30
  import { registerLaneReportCommands } from "./lanes-report.js";
30
31
  import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
31
32
  import { registerLaneReviewCommand } from "./lanes-review.js";
@@ -34,7 +35,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
34
35
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
35
36
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
36
37
  import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
37
- import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG } from "../lib/lanes/flag-keys.js";
38
+ import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, } from "../lib/lanes/flag-keys.js";
38
39
  import { CODEX_ROLLOUT_RETENTION_FLAG, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
39
40
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
40
41
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -1336,6 +1337,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
1336
1337
  updated: new Map(),
1337
1338
  unverifiedRemote: new Map(),
1338
1339
  missingOnOwner: new Map(),
1340
+ skippedRemoteRefresh: new Set(),
1339
1341
  };
1340
1342
  const now = ctx.now();
1341
1343
  const nowMs = now.getTime();
@@ -1377,6 +1379,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
1377
1379
  if (candidates.length === 0)
1378
1380
  return result;
1379
1381
  const enabledByCompany = new Map();
1382
+ const skipRefreshByCompany = new Map();
1380
1383
  const enabledCandidates = [];
1381
1384
  for (const lane of candidates) {
1382
1385
  const company = resolveLaneCompany(hqRoot, lane).company;
@@ -1391,8 +1394,20 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
1391
1394
  });
1392
1395
  enabledByCompany.set(companyKey, enabled);
1393
1396
  }
1394
- if (enabled)
1395
- enabledCandidates.push(lane);
1397
+ if (!enabled)
1398
+ continue;
1399
+ if (options.skipRemoteRefreshForCompany) {
1400
+ let skipRefresh = skipRefreshByCompany.get(companyKey);
1401
+ if (skipRefresh === undefined) {
1402
+ skipRefresh = await options.skipRemoteRefreshForCompany(company);
1403
+ skipRefreshByCompany.set(companyKey, skipRefresh);
1404
+ }
1405
+ if (skipRefresh) {
1406
+ result.skippedRemoteRefresh.add(lane.lane_id);
1407
+ continue;
1408
+ }
1409
+ }
1410
+ enabledCandidates.push(lane);
1396
1411
  }
1397
1412
  if (enabledCandidates.length === 0)
1398
1413
  return result;
@@ -4949,7 +4964,12 @@ async function runList(ctx, opts) {
4949
4964
  }
4950
4965
  const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
4951
4966
  const remoteRefresh = remoteRead
4952
- ? { updated: new Map(), unverifiedRemote: new Map(), missingOnOwner: new Map() }
4967
+ ? {
4968
+ updated: new Map(),
4969
+ unverifiedRemote: new Map(),
4970
+ missingOnOwner: new Map(),
4971
+ skippedRemoteRefresh: new Set(),
4972
+ }
4953
4973
  : await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(laneSource
4954
4974
  .filter((lane) => matchesLaneFilters(lane, false))
4955
4975
  .map((lane) => lane.lane_id)), {
@@ -4957,6 +4977,7 @@ async function runList(ctx, opts) {
4957
4977
  laneIds: new Set(laneSource
4958
4978
  .filter((lane) => matchesLaneFilters(lane, false))
4959
4979
  .map((lane) => lane.lane_id)),
4980
+ skipRemoteRefreshForCompany: (company) => readLanesRegistryFlagFailOff(LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, { company, env: ctx.env }),
4960
4981
  });
4961
4982
  for (const [laneId, updated] of remoteRefresh.updated) {
4962
4983
  const index = laneSource.findIndex((lane) => lane.lane_id === laneId);
@@ -4994,6 +5015,7 @@ async function runList(ctx, opts) {
4994
5015
  const ownerEligibleLaneIds = new Set();
4995
5016
  for (const lane of selectedLanes) {
4996
5017
  if (remoteRead ||
5018
+ remoteRefresh.skippedRemoteRefresh.has(lane.lane_id) ||
4997
5019
  lane.host.kind !== "outpost" ||
4998
5020
  isTerminalLaneState(lane.state) ||
4999
5021
  laneRunsHere(lane, host)) {
@@ -5033,6 +5055,7 @@ async function runList(ctx, opts) {
5033
5055
  const livenessBySession = new Map();
5034
5056
  let livenessDeps;
5035
5057
  const lanes = selectedLanes.map((lane) => {
5058
+ const overdue = Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now);
5036
5059
  const remoteQueued = lane.state === "queued" &&
5037
5060
  lane.host.kind === "outpost" &&
5038
5061
  !laneRunsHere(lane, host);
@@ -5051,7 +5074,7 @@ async function runList(ctx, opts) {
5051
5074
  queued_details: lane.queued_reason === "low_disk" ? lane.queued_details : undefined,
5052
5075
  }
5053
5076
  : {}),
5054
- overdue: Boolean(lane.overdue) || isOverdue(hqRoot, lane.lane_id, now),
5077
+ overdue,
5055
5078
  stalled: Boolean(lane.stalled),
5056
5079
  // Listing is observational. Reconcile owns durable liveness transitions.
5057
5080
  senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
@@ -7872,6 +7895,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7872
7895
  updated: new Map(),
7873
7896
  unverifiedRemote: new Map(),
7874
7897
  missingOnOwner: new Map(),
7898
+ skippedRemoteRefresh: new Set(),
7875
7899
  }
7876
7900
  : await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot));
7877
7901
  const remoteLost = [];
@@ -8322,6 +8346,21 @@ export function registerLanesCommand(parent, deps = {}) {
8322
8346
  deps: ctx.livenessDeps,
8323
8347
  }),
8324
8348
  }, wrap);
8349
+ registerLaneDropboxRepairCommand(lanes, {
8350
+ hqRoot: ctx.hqRoot,
8351
+ now: ctx.now,
8352
+ resolveCompany: ctx.resolveCompany,
8353
+ repairEnabled: async (lane) => {
8354
+ const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
8355
+ const company = resolution.company ?? undefined;
8356
+ return readLanesRegistryFlagFailOff(LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, {
8357
+ company,
8358
+ ...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
8359
+ env: ctx.env,
8360
+ }, readLanesRegistryFlag);
8361
+ },
8362
+ wrap,
8363
+ });
8325
8364
  registerLaneReportCommands(lanes, {
8326
8365
  hqRoot: ctx.hqRoot,
8327
8366
  resolveCompany: ctx.resolveCompany,
package/dist/fast-core.js CHANGED
@@ -9,6 +9,7 @@
9
9
  import { resolveLiveRoot } from "./utils/hq-roots.js";
10
10
  import { runBundledScript } from "./utils/run-bundled-script.js";
11
11
  import { parseFastCore } from "./fast-scaffold.js";
12
+ import { CORE_RRR_WORKTREE_SCOPE_FIX_FLAG } from "./commands/core-rrr-worktree-scope-fix-flag.js";
12
13
  export async function readStdinIfPiped(stream) {
13
14
  if (stream.isTTY)
14
15
  return "";
@@ -35,6 +36,9 @@ export async function runFastCore(argv) {
35
36
  familyEnabled = envOverride ?? await (await import("./native-fast-route.js")).fastFlagEnabled(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
36
37
  }
37
38
  const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
39
+ const worktreeScopeFixEnabled = entry.name === "repo-run-registry"
40
+ ? await (await import("./native-fast-route.js")).fastFlagEnabled(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, process.env, { numericEnvironmentOverride: false })
41
+ : undefined;
38
42
  const shouldInjectRoot = useShell
39
43
  ? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
40
44
  !parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
@@ -69,7 +73,7 @@ export async function runFastCore(argv) {
69
73
  }
70
74
  const readsStdin = entry.readsStdinFor?.(parsed.args) ?? entry.readsStdin ?? false;
71
75
  const stdin = readsStdin ? await readStdinIfPiped(process.stdin) : "";
72
- const code = await handler(parsed.args, stdin, process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
76
+ const code = await handler(parsed.args, stdin, process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg, worktreeScopeFixEnabled);
73
77
  // Let Node drain handler output before terminating. The timeout guard's
74
78
  // stderr message must survive the native route's process boundary.
75
79
  process.exitCode = code;
@@ -90,6 +94,7 @@ export async function runFastCore(argv) {
90
94
  ...(entry.name === "migrate-policy-triggers"
91
95
  ? { CLAUDE_PROJECT_DIR: process.env.CLAUDE_PROJECT_DIR || process.cwd() }
92
96
  : {}),
97
+ ...(entry.name === "repo-run-registry" ? { HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX: String(worktreeScopeFixEnabled) } : {}),
93
98
  };
94
99
  const { code } = runBundledScript({
95
100
  asset,
package/dist/index.d.ts CHANGED
@@ -3,8 +3,17 @@ import "./node-preflight.js";
3
3
  import "./node-network-compat-fast.js";
4
4
  import { isVersionRequest } from "./version-request.js";
5
5
  import { isFastCoreRequest } from "./fast-scaffold.js";
6
+ type PackageUseLeaseLoader = () => Promise<void>;
7
+ declare function isNpmPackageEntry(entryUrl: string): boolean;
8
+ declare function dispatchWithPackageUseLease(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
9
+ declare function runCliEntry(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
6
10
  export declare const __test__: {
7
11
  isVersionRequest: typeof isVersionRequest;
8
12
  isFastCoreRequest: typeof isFastCoreRequest;
13
+ dispatchWithPackageUseLease: typeof dispatchWithPackageUseLease;
14
+ runCliEntry: typeof runCliEntry;
15
+ isNpmPackageEntry: typeof isNpmPackageEntry;
16
+ acquirePackageUseLease: (prefix: string, version: string, stateDirectory?: string) => Promise<import("./utils/package-use-lease.js").PackageUseLease>;
9
17
  };
18
+ export {};
10
19
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -17,77 +17,149 @@ import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.
17
17
  // command never run (HQ-CLI-W). Sets process.exitCode only, never process.exit,
18
18
  // so runCli's finally still finalizes release health and flushes Sentry.
19
19
  installProcessRejectionBoundary();
20
- if (isVersionRequest(process.argv)) {
21
- // Bare `--version` is the CLI semver only (the contract field updaters
22
- // compare). `hq version` and `--json` print the labelled HQ/CLI report.
23
- // Lazy so this path still skips the command graph.
24
- if (wantsLabelledVersionReport(process.argv)) {
25
- void import("./lib/flag-registry.js").then(({ kickFlagRegistryReadiness }) => {
26
- kickFlagRegistryReadiness();
27
- return import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
20
+ function dispatchCliEntry() {
21
+ if (isVersionRequest(process.argv)) {
22
+ // Bare `--version` is the CLI semver only (the contract field updaters
23
+ // compare). `hq version` and `--json` print the labelled HQ/CLI report.
24
+ // Lazy so this path still skips the command graph.
25
+ if (wantsLabelledVersionReport(process.argv)) {
26
+ void import("./lib/flag-registry.js").then(({ kickFlagRegistryReadiness }) => {
27
+ kickFlagRegistryReadiness();
28
+ return import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
29
+ });
30
+ }
31
+ else {
32
+ // Keep the benchmarked updater fast route limited to the installed semver.
33
+ // Registry checks and flag resolution belong to the regular command path.
34
+ void import("./cli-version.js").then(({ CLI_VERSION }) => {
35
+ process.stdout.write(`${CLI_VERSION}\n`);
36
+ });
37
+ }
38
+ }
39
+ else if (parseFastCore(process.argv)?.entry.kind === "native" &&
40
+ (process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
41
+ process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
42
+ // An explicit enabled override has already decided the global gate. Skip the
43
+ // snapshot reader import on hot native plumbing paths; runFastCore still
44
+ // checks the command-family flag before choosing native or shell rollback.
45
+ void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
46
+ }
47
+ else if (parseFastCore(process.argv)?.entry.kind === "native") {
48
+ // Native routes are default-off. Resolve the company-scoped persisted flag
49
+ // without loading the command graph; a miss or stale snapshot takes the
50
+ // existing full-graph route.
51
+ void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
52
+ if (await nativeFastRouteEnabled(process.argv)) {
53
+ const { runFastCore } = await import("./fast-core.js");
54
+ await runFastCore(process.argv);
55
+ }
56
+ else {
57
+ const { runCli } = await import("./main.js");
58
+ await runCli();
59
+ }
28
60
  });
29
61
  }
30
- else {
31
- // Keep the benchmarked updater fast route limited to the installed semver.
32
- // Registry checks and flag resolution belong to the regular command path.
33
- void import("./cli-version.js").then(({ CLI_VERSION }) => {
34
- process.stdout.write(`${CLI_VERSION}\n`);
62
+ else if (parseSecretsEnvFastRequest(process.argv)) {
63
+ const request = parseSecretsEnvFastRequest(process.argv);
64
+ void import("./secrets-env-fast-route.js").then(async (route) => {
65
+ await route.dispatchSecretsEnvFastRequest(request, {
66
+ isEnabled: () => route.secretsEnvFastRouteEnabled(request),
67
+ runFast: async () => {
68
+ const { runSecretsEnvFastPath } = await import("./secrets-env-fast-path.js");
69
+ return runSecretsEnvFastPath(request);
70
+ },
71
+ runCurrent: async () => {
72
+ const { runCli } = await import("./main.js");
73
+ await runCli();
74
+ },
75
+ });
35
76
  });
36
77
  }
78
+ else if (isFastCoreRequest(process.argv)) {
79
+ // Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
80
+ // dispatches straight to the bundled-script runner, so it never pays for the
81
+ // ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
82
+ void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
83
+ }
84
+ else {
85
+ // Keep the command lifecycle detached from module evaluation, as it was
86
+ // before the fast --version split. Some best-effort teardown work uses
87
+ // unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
88
+ // successful command into exit 13 when that teardown promise remains pending.
89
+ // A floated rejection here no longer exits 0 silently: the boundary installed
90
+ // above turns it into a non-zero exit (HQ-CLI-W).
91
+ void import("./main.js").then(({ runCli }) => runCli());
92
+ }
37
93
  }
38
- else if (parseFastCore(process.argv)?.entry.kind === "native" &&
39
- (process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
40
- process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
41
- // An explicit enabled override has already decided the global gate. Skip the
42
- // snapshot reader import on hot native plumbing paths; runFastCore still
43
- // checks the command-family flag before choosing native or shell rollback.
44
- void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
94
+ function isNpmPackageEntry(entryUrl) {
95
+ return /\/node_modules\/@indigoai-us\/hq-cli\/dist\/index\.js$/i.test(entryUrl);
45
96
  }
46
- else if (parseFastCore(process.argv)?.entry.kind === "native") {
47
- // Native routes are default-off. Resolve the company-scoped persisted flag
48
- // without loading the command graph; a miss or stale snapshot takes the
49
- // existing full-graph route.
50
- void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
51
- if (await nativeFastRouteEnabled(process.argv)) {
52
- const { runFastCore } = await import("./fast-core.js");
53
- await runFastCore(process.argv);
54
- }
55
- else {
56
- const { runCli } = await import("./main.js");
57
- await runCli();
58
- }
59
- });
97
+ function isPackageUseUpdatePendingError(error) {
98
+ return Boolean(error &&
99
+ typeof error === "object" &&
100
+ error.name === "PackageUseUpdatePendingError");
60
101
  }
61
- else if (parseSecretsEnvFastRequest(process.argv)) {
62
- const request = parseSecretsEnvFastRequest(process.argv);
63
- void import("./secrets-env-fast-route.js").then(async (route) => {
64
- await route.dispatchSecretsEnvFastRequest(request, {
65
- isEnabled: () => route.secretsEnvFastRouteEnabled(request),
66
- runFast: async () => {
67
- const { runSecretsEnvFastPath } = await import("./secrets-env-fast-path.js");
68
- return runSecretsEnvFastPath(request);
69
- },
70
- runCurrent: async () => {
71
- const { runCli } = await import("./main.js");
72
- await runCli();
73
- },
74
- });
75
- });
102
+ function packageUseErrorClass(error) {
103
+ if (!error || typeof error !== "object")
104
+ return "unknown";
105
+ const value = error;
106
+ if (typeof value.code === "string")
107
+ return value.code;
108
+ if (value.cause && typeof value.cause === "object") {
109
+ const cause = value.cause;
110
+ if (typeof cause.code === "string")
111
+ return cause.code;
112
+ if (typeof cause.name === "string")
113
+ return cause.name;
114
+ }
115
+ return typeof value.name === "string" ? value.name : "unknown";
76
116
  }
77
- else if (isFastCoreRequest(process.argv)) {
78
- // Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
79
- // dispatches straight to the bundled-script runner, so it never pays for the
80
- // ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
81
- void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
117
+ function dispatchWithPackageUseLease(entryUrl, dispatch, loadLease = async () => {
118
+ const { acquireCurrentPackageUseLease } = await import("./utils/package-use-lease.js");
119
+ acquireCurrentPackageUseLease();
120
+ }) {
121
+ if (!isNpmPackageEntry(entryUrl)) {
122
+ dispatch();
123
+ return;
124
+ }
125
+ return loadLease().then(dispatch, (error) => {
126
+ if (isPackageUseUpdatePendingError(error))
127
+ throw error;
128
+ process.stderr.write(`hq-cli package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
129
+ dispatch();
130
+ });
82
131
  }
83
- else {
84
- // Keep the command lifecycle detached from module evaluation, as it was
85
- // before the fast --version split. Some best-effort teardown work uses
86
- // unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
87
- // successful command into exit 13 when that teardown promise remains pending.
88
- // A floated rejection here no longer exits 0 silently: the boundary installed
89
- // above turns it into a non-zero exit (HQ-CLI-W).
90
- void import("./main.js").then(({ runCli }) => runCli());
132
+ function runCliEntry(entryUrl, dispatch, loadLease) {
133
+ const handlePendingUpdate = (error) => {
134
+ if (!isPackageUseUpdatePendingError(error))
135
+ throw error;
136
+ const message = error instanceof Error
137
+ ? error.message
138
+ : "The HQ CLI package is being updated";
139
+ process.stderr.write(`hq-cli: ${message}\n`);
140
+ process.exitCode = 1;
141
+ };
142
+ try {
143
+ const result = dispatchWithPackageUseLease(entryUrl, dispatch, loadLease);
144
+ return result?.catch(handlePendingUpdate);
145
+ }
146
+ catch (error) {
147
+ handlePendingUpdate(error);
148
+ }
91
149
  }
92
- export const __test__ = { isVersionRequest, isFastCoreRequest };
150
+ // Local builds and benchmark fixtures are not installed in an npm prefix.
151
+ // Keep their fast route free of package-use filesystem work; npm installs still
152
+ // publish a lease before dispatch.
153
+ void runCliEntry(import.meta.url, dispatchCliEntry);
154
+ export const __test__ = {
155
+ isVersionRequest,
156
+ isFastCoreRequest,
157
+ dispatchWithPackageUseLease,
158
+ runCliEntry,
159
+ isNpmPackageEntry,
160
+ acquirePackageUseLease: async (prefix, version, stateDirectory) => {
161
+ const lease = await import("./utils/package-use-lease.js");
162
+ return lease.acquirePackageUseLease(prefix, version, stateDirectory);
163
+ },
164
+ };
93
165
  //# sourceMappingURL=index.js.map
@@ -12,6 +12,8 @@ import { type HostApp } from "./supervisor.js";
12
12
  export declare const LOG_ROTATE_BYTES: number;
13
13
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
14
14
  export declare const RESTART_EXIT_CODE = 75;
15
+ /** Route an updater request through the daemon's existing graceful handoff. */
16
+ export declare function handoffForPackageUpdateRequest(requested: boolean, restartDaemon: () => void): void;
15
17
  export interface RunDaemonOptions {
16
18
  foreground?: boolean;
17
19
  /** Started by the OS unit, which relaunches the daemon whenever it exits. */
@@ -12,6 +12,7 @@ import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { loadCachedTokens } from "@indigoai-us/hq-cloud";
14
14
  import { CLI_VERSION } from "../../cli-version.js";
15
+ import { currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
15
16
  import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
16
17
  import { agentKitPaths } from "../agent-kit/paths.js";
17
18
  import { readBotConfig } from "../bot/config.js";
@@ -39,6 +40,11 @@ import { userToolDirs } from "./tools.js";
39
40
  export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
40
41
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
41
42
  export const RESTART_EXIT_CODE = 75;
43
+ /** Route an updater request through the daemon's existing graceful handoff. */
44
+ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
45
+ if (requested)
46
+ restartDaemon();
47
+ }
42
48
  const LEGACY_PROBE_TTL_MS = 60_000;
43
49
  /** A flag read that has not answered by then counts as off. */
44
50
  const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
@@ -603,7 +609,10 @@ export async function runDaemon(opts) {
603
609
  log("info", `rotated ${n} log file(s)`);
604
610
  return true;
605
611
  },
606
- takeRequests: () => takeControlRequests(paths.requestsDir),
612
+ takeRequests: () => {
613
+ handoffForPackageUpdateRequest(currentPackageUseUpdateRequested(), restartDaemon);
614
+ return takeControlRequests(paths.requestsDir);
615
+ },
607
616
  restartDaemon: () => restartDaemon(),
608
617
  writeState: (state) => writeJsonAtomic(paths.statePath, state),
609
618
  log,
@@ -6,7 +6,7 @@ import { isControllerAliasOutpostId } from "./admission-host.js";
6
6
  import { LEAD_REGISTRY_REL, validateOutpostLeadRegistry } from "./autopilot-registry.js";
7
7
  import { appendDropboxMessage } from "./dropbox.js";
8
8
  import { LanesError } from "./errors.js";
9
- import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_FLAG, LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG, LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG as PER_LEAD_ERRORS_FLAG, } from "./flag-keys.js";
9
+ import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_FLAG, LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG, LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG, LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG as PER_LEAD_ERRORS_FLAG, } from "./flag-keys.js";
10
10
  import { resolveLaneCompany } from "./lane-company.js";
11
11
  import { readLanesRegistryFlag } from "./registry-flag.js";
12
12
  import { readAllLanes } from "./store.js";
@@ -786,9 +786,16 @@ async function runLeadAutopilotPassUnlocked(input) {
786
786
  result.skipped.push({ lead_lane: lead.lane_id, reason: "lead_company_unresolved" });
787
787
  continue;
788
788
  }
789
+ // A lead created on an Outpost records a local host, while its registry row names the Outpost.
790
+ const outpostNativeLead = !registeredLocalHost &&
791
+ input.outpostId === registered.host &&
792
+ lead.host.kind === "local" &&
793
+ lead.host.id === input.hostname &&
794
+ await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG, { company }).catch(() => false);
789
795
  const hostMatchesRegistry = registeredLocalHost
790
796
  ? lead.host.kind === "local"
791
- : lead.host.kind === "outpost" && lead.host.id === registered.host && input.outpostId === registered.host;
797
+ : outpostNativeLead ||
798
+ (lead.host.kind === "outpost" && lead.host.id === registered.host && input.outpostId === registered.host);
792
799
  if (!hostMatchesRegistry ||
793
800
  !laneRunsOnHost(lead, { hostname: input.hostname, outpostId: effectiveOutpostId })) {
794
801
  result.skipped.push({ lead_lane: lead.lane_id, reason: "lead_host_mismatch" });
@@ -4,6 +4,9 @@ export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-bala
4
4
  export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
5
5
  /** Count only children with live work against their senior's hire grant. */
6
6
  export declare const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
7
+ /** Skips owner refreshes during ordinary lane list polling when explicitly enabled. */
8
+ export declare const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
9
+ export declare const LANE_LIST_SKIP_REMOTE_REFRESH_DEFAULT_ENABLED = false;
7
10
  /** Enables missed-deadline lead nudges and second-miss operator escalation. */
8
11
  export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
9
12
  export declare const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
@@ -22,4 +25,10 @@ export declare const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
22
25
  /** Enables staleness notices for a registered lead's dashboard headline. */
23
26
  export declare const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-headline-staleness";
24
27
  export declare const LEAD_AUTOPILOT_HEADLINE_STALENESS_DEFAULT_ENABLED = false;
28
+ /** Lets an Outpost's autopilot wake leads created on that Outpost, which record a local host. */
29
+ export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost-native-leads";
30
+ export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_DEFAULT_ENABLED = false;
31
+ /** Allows applying an operator-reviewed repair of messages stranded in bare-ULID dropboxes. */
32
+ export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
33
+ export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_DEFAULT_ENABLED = false;
25
34
  //# sourceMappingURL=flag-keys.d.ts.map
@@ -4,6 +4,9 @@ export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
4
4
  export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
5
5
  /** Count only children with live work against their senior's hire grant. */
6
6
  export const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
7
+ /** Skips owner refreshes during ordinary lane list polling when explicitly enabled. */
8
+ export const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
9
+ export const LANE_LIST_SKIP_REMOTE_REFRESH_DEFAULT_ENABLED = false;
7
10
  /** Enables missed-deadline lead nudges and second-miss operator escalation. */
8
11
  export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
9
12
  export const LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
@@ -22,4 +25,10 @@ export const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
22
25
  /** Enables staleness notices for a registered lead's dashboard headline. */
23
26
  export const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-headline-staleness";
24
27
  export const LEAD_AUTOPILOT_HEADLINE_STALENESS_DEFAULT_ENABLED = false;
28
+ /** Lets an Outpost's autopilot wake leads created on that Outpost, which record a local host. */
29
+ export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost-native-leads";
30
+ export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_DEFAULT_ENABLED = false;
31
+ /** Allows applying an operator-reviewed repair of messages stranded in bare-ULID dropboxes. */
32
+ export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
33
+ export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_DEFAULT_ENABLED = false;
25
34
  //# sourceMappingURL=flag-keys.js.map
@@ -1,4 +1,4 @@
1
- type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "not_restarted" | "waiting";
1
+ type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "not_restarted" | "keep_alive_off" | "waiting";
2
2
  interface KeepAliveAuditRestart {
3
3
  at: string;
4
4
  trigger: string;
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS } from "./keep-alive.js";
4
4
  import { LanesError } from "./errors.js";
5
5
  import { laneRunDir } from "./spawn.js";
6
- import { readAllLanes } from "./store.js";
6
+ import { listQuestionIds, readAllLanes, tryReadQuestion } from "./store.js";
7
7
  function readDoneClassifications(hqRoot, laneId) {
8
8
  const root = fs.realpathSync(hqRoot);
9
9
  const watcherLog = path.join(laneRunDir(root, laneId), "logs", "watcher.out");
@@ -56,6 +56,7 @@ const VERDICTS = [
56
56
  "message_wake",
57
57
  "self_wake",
58
58
  "early_by_keep_alive",
59
+ "keep_alive_off",
59
60
  "not_restarted",
60
61
  "waiting",
61
62
  ];
@@ -219,9 +220,66 @@ function hasDeliveredDropboxEvidence(hqRoot, laneId, stoppedAt, restartAt) {
219
220
  const realFile = fs.realpathSync(file);
220
221
  if (!withinRoot(root, realFile))
221
222
  return false;
222
- return stat.mtimeMs >= stoppedAt && stat.mtimeMs <= restartAt + 2 * 60_000;
223
+ if (stat.mtimeMs >= stoppedAt && stat.mtimeMs <= restartAt + 2 * 60_000)
224
+ return true;
225
+ if (!name.endsWith(".inbox.jsonl") && !name.endsWith(".resume.jsonl"))
226
+ return false;
227
+ const text = fs.readFileSync(realFile, "utf8");
228
+ for (const line of text.split(/\r?\n/)) {
229
+ if (!line.trim())
230
+ continue;
231
+ let value;
232
+ try {
233
+ value = JSON.parse(line);
234
+ }
235
+ catch (err) {
236
+ throw new LanesError("keep_alive_audit_event_invalid", `Cannot parse delivered lane message ${realFile}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: laneId, path: realFile });
237
+ }
238
+ if (isRecord(value)) {
239
+ const at = parseTime(value.at);
240
+ if (at !== undefined && at >= stoppedAt && at <= restartAt + 2 * 60_000)
241
+ return true;
242
+ }
243
+ }
244
+ return false;
223
245
  });
224
246
  }
247
+ function hasMessageResumeEvidence(lane, stoppedAt, restartAt) {
248
+ return (lane.resume_history ?? []).some((entry) => (entry.trigger === "message" || entry.trigger === "answered_questions") &&
249
+ (parseTime(entry.at) ?? Number.NaN) >= stoppedAt &&
250
+ (parseTime(entry.at) ?? Number.NaN) <= restartAt + 2 * 60_000);
251
+ }
252
+ function hasAnsweredQuestionEvidence(hqRoot, laneId, stoppedAt, restartAt) {
253
+ for (const questionId of listQuestionIds(hqRoot)) {
254
+ let question;
255
+ try {
256
+ question = tryReadQuestion(hqRoot, questionId);
257
+ }
258
+ catch {
259
+ // One unreadable question file must not fail the whole audit.
260
+ continue;
261
+ }
262
+ if (!question || question.lane_id !== laneId || question.origin?.id !== laneId)
263
+ continue;
264
+ const timestamps = [
265
+ question.delivered_at,
266
+ question.answer?.answered_at,
267
+ ...(question.answer_history ?? []).map((answer) => answer.answered_at),
268
+ ];
269
+ if (timestamps.some((timestamp) => {
270
+ const at = parseTime(timestamp);
271
+ return at !== undefined && at >= stoppedAt && at <= restartAt + 2 * 60_000;
272
+ }))
273
+ return true;
274
+ }
275
+ return false;
276
+ }
277
+ function laneWasKeepAliveOffAtStop(lane, stoppedAt) {
278
+ if (lane.keep_alive === true)
279
+ return false;
280
+ const optedInAt = parseTime(lane.keep_alive_opted_in_at);
281
+ return optedInAt === undefined || optedInAt > stoppedAt;
282
+ }
225
283
  function firstRestartAfter(lane, stop, nextEnvelopeMs) {
226
284
  const restart = (lane.resume_history ?? [])
227
285
  .map((entry) => ({ entry, at: parseTime(entry.at) }))
@@ -241,13 +299,18 @@ function classifyVerdict(input) {
241
299
  const restartAt = Date.parse(restart.at);
242
300
  if (restart.caller === lane.lane_id)
243
301
  return "self_wake";
302
+ if (restart.trigger === "message" || restart.trigger === "answered_questions")
303
+ return "message_wake";
244
304
  if (restart.trigger === "keep_alive") {
245
305
  const event = input.resumeEvents
246
- .filter((candidate) => candidate.at > input.stoppedAt && Math.abs(candidate.at - restartAt) <= 2 * 60_000)
306
+ .filter((candidate) => candidate.at > input.stoppedAt && candidate.at <= restartAt + 2 * 60_000)
247
307
  .sort((left, right) => Math.abs(left.at - restartAt) - Math.abs(right.at - restartAt))[0];
248
308
  if (event?.reason === "pending_work")
249
309
  return "message_wake";
250
- if (event?.reason === undefined && hasDeliveredDropboxEvidence(input.hqRoot, lane.lane_id, input.stoppedAt, restartAt)) {
310
+ if (event?.reason === undefined &&
311
+ (hasDeliveredDropboxEvidence(input.hqRoot, lane.lane_id, input.stoppedAt, restartAt) ||
312
+ hasMessageResumeEvidence(lane, input.stoppedAt, restartAt) ||
313
+ hasAnsweredQuestionEvidence(input.hqRoot, lane.lane_id, input.stoppedAt, restartAt))) {
251
314
  return "message_wake";
252
315
  }
253
316
  const nextCheckAt = parseTime(input.nextCheckAt);
@@ -264,6 +327,8 @@ function classifyVerdict(input) {
264
327
  return "early_wake";
265
328
  return "late";
266
329
  }
330
+ if (laneWasKeepAliveOffAtStop(lane, input.stoppedAt))
331
+ return "keep_alive_off";
267
332
  if (lane.state === "done" && nowMs > dueAt + KEEP_ALIVE_NOT_RESTARTED_GRACE_MS)
268
333
  return "not_restarted";
269
334
  return "waiting";