@indigoai-us/hq-cli 5.345.42 → 5.345.44

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 (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/command-catalog.generated.d.ts +2 -2
  3. package/dist/command-catalog.generated.js +2 -2
  4. package/dist/commands/db-remote-deps.js +4 -0
  5. package/dist/commands/install-global.d.ts +8 -1
  6. package/dist/commands/install-global.js +128 -25
  7. package/dist/commands/lanes-checks.d.ts +1 -0
  8. package/dist/commands/lanes-checks.js +6 -1
  9. package/dist/commands/lanes-message.js +1 -0
  10. package/dist/commands/lanes-report.d.ts +2 -1
  11. package/dist/commands/lanes-report.js +63 -6
  12. package/dist/commands/lanes-resume.js +3 -0
  13. package/dist/commands/lanes.js +218 -49
  14. package/dist/index.js +43 -7
  15. package/dist/lib/anywhere-person-setting.d.ts +2 -0
  16. package/dist/lib/anywhere-person-setting.js +2 -1
  17. package/dist/lib/anywhere-runtime-fast-flag.js +8 -2
  18. package/dist/lib/core-utils/sentry-report.js +8 -1
  19. package/dist/lib/daemon/anywhere-listener.d.ts +16 -0
  20. package/dist/lib/daemon/anywhere-listener.js +79 -0
  21. package/dist/lib/daemon/run.js +30 -23
  22. package/dist/lib/db/control-plane.d.ts +5 -0
  23. package/dist/lib/db/control-plane.js +7 -0
  24. package/dist/lib/doctor/checks/anywhere.d.ts +10 -0
  25. package/dist/lib/doctor/checks/anywhere.js +45 -17
  26. package/dist/lib/flag-registry.d.ts +4 -0
  27. package/dist/lib/flag-registry.js +43 -2
  28. package/dist/lib/flag-snapshot-file.d.ts +17 -0
  29. package/dist/lib/flag-snapshot-file.js +153 -37
  30. package/dist/lib/lanes/flag-keys.d.ts +5 -0
  31. package/dist/lib/lanes/flag-keys.js +5 -0
  32. package/dist/lib/lanes/grants.js +1 -1
  33. package/dist/lib/lanes/reconcile.d.ts +2 -0
  34. package/dist/lib/lanes/reconcile.js +39 -1
  35. package/dist/lib/lanes/spawn.d.ts +4 -0
  36. package/dist/lib/lanes/spawn.js +132 -6
  37. package/dist/lib/lanes/store.js +35 -0
  38. package/dist/lib/lanes/tracker-report.d.ts +46 -0
  39. package/dist/lib/lanes/tracker-report.js +85 -1
  40. package/dist/lib/lanes/types.d.ts +3 -1
  41. package/dist/lib/lanes/types.js +1 -0
  42. package/dist/main.js +5 -1
  43. package/dist/startup-registration.d.ts +3 -0
  44. package/dist/startup-registration.js +11 -4
  45. package/dist/utils/resident-package-preload.js +5 -1
  46. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.44] — 2026-10-07
6
+ - Quota-exhausted lanes can fail over within their provider, hold with per-account eligibility reasons when none is available, and retry when the earliest quota reset arrives. The default-off `lanes.quota-account-failover` gate controls the new recovery path.
7
+
8
+ - When a company's plan does not include remote databases, `hq db provision`, `usage`, `sql`, `dump` and `destroy` now show which plan includes them, the $10 per database app price and the upgrade link.
9
+ - `hq lanes report` can show a gated focus list, focus-guard override reasons, and per-team 24-hour focus share.
10
+ - Return a child's hire grant to its senior when the lane becomes terminal, let the senior adjust or clear the saved grant, report grant updates correctly, and reject over-cap increases while allowing reductions that repair oversubscription.
11
+ - Recover CLI startup when a package update temporarily removes a module from the installed command graph.
12
+
13
+
14
+ ## [5.345.43] — 2026-10-07
15
+ - The published core-command E2E workflow now retries the exact npm release install for up to 10 minutes while registry propagation completes.
16
+
17
+ - HQ Anywhere resolves person-level flags in unlinked folders. The daemon refreshes the flag and person setting every 60 seconds, then opens or closes its socket when either changes.
18
+ - Claude and Codex global installs use one skill prefix, keep company skills out of global scope, remove old company links during upgrades, and skip broken sources. Dry runs use `~/.hq/root` when run outside a checkout.
19
+ - Doctor checks for a live desktop-hosted daemon instead of treating the sync LaunchAgent as proof. Registry refreshes retain the daemon's machine scope and ID token.
20
+ - Let a failed lane's ancestors or same-company manager resume it, and backfill missing company attribution when reparenting it to a company senior.
21
+ - Let Claude lanes fall back to eligible accounts when every profile-pinned account is unavailable, behind the default-off `lanes.profile-pin-exhausted-fallback` flag.
22
+
5
23
  ## [5.345.42] — 2026-10-07
6
24
  - `hq db usage --app <name>` shows one database app's included amounts, use, overage, read-only ceiling and estimated monthly charge.
7
25
 
@@ -12464,7 +12464,7 @@ export declare const COMMAND_CATALOG: readonly [{
12464
12464
  readonly subcommands: readonly [];
12465
12465
  }, {
12466
12466
  readonly name: "resume";
12467
- readonly description: "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option";
12467
+ readonly description: "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with --from-failed as a senior ancestor or same-company manager";
12468
12468
  readonly aliases: readonly [];
12469
12469
  readonly hidden: false;
12470
12470
  readonly helpOption: true;
@@ -12483,7 +12483,7 @@ export declare const COMMAND_CATALOG: readonly [{
12483
12483
  readonly hidden: false;
12484
12484
  }, {
12485
12485
  readonly flags: "--from-failed";
12486
- readonly description: "Explicitly resume a failed lane as its senior";
12486
+ readonly description: "Explicitly resume a failed lane as an ancestor or same-company manager";
12487
12487
  readonly hidden: false;
12488
12488
  }, {
12489
12489
  readonly flags: "--reason <text>";
@@ -15156,7 +15156,7 @@ export const COMMAND_CATALOG = [
15156
15156
  },
15157
15157
  {
15158
15158
  "name": "resume",
15159
- "description": "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option",
15159
+ "description": "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with --from-failed as a senior ancestor or same-company manager",
15160
15160
  "aliases": [],
15161
15161
  "hidden": false,
15162
15162
  "helpOption": true,
@@ -15179,7 +15179,7 @@ export const COMMAND_CATALOG = [
15179
15179
  },
15180
15180
  {
15181
15181
  "flags": "--from-failed",
15182
- "description": "Explicitly resume a failed lane as its senior",
15182
+ "description": "Explicitly resume a failed lane as an ancestor or same-company manager",
15183
15183
  "hidden": false
15184
15184
  },
15185
15185
  {
@@ -42,6 +42,10 @@ export function describeRemoteError(error) {
42
42
  const status = error.status;
43
43
  const code = error.code;
44
44
  if (code === "REMOTE_DB_NOT_AVAILABLE" || status === 501) {
45
+ const reason = error.reason;
46
+ const message = error.message;
47
+ if (reason === "plan" && typeof message === "string" && message)
48
+ return message;
45
49
  return "Remote databases are not available yet.";
46
50
  }
47
51
  if (status === 402 || code === "PLAN_REQUIRED") {
@@ -75,8 +75,15 @@ export interface GlobalCommandOpts {
75
75
  log?: (line: string) => void;
76
76
  /** Test seam for the hq-pro person preference read. */
77
77
  personSettingReader?: () => Promise<boolean | null>;
78
+ /** Root-discovery seams used by command tests and wrappers. */
79
+ home?: string;
80
+ cwd?: string;
78
81
  }
79
- export declare function resolveInstallRoot(explicit: string | undefined): string;
82
+ export declare function resolveInstallRoot(explicit: string | undefined, options?: {
83
+ env?: NodeJS.ProcessEnv;
84
+ cwd?: string;
85
+ home?: string;
86
+ }): string;
80
87
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
81
88
  export declare function runInstallGlobal(opts: GlobalCommandOpts): Promise<void>;
82
89
  export declare function registerUninstallCommand(program: Command): void;
@@ -9,6 +9,7 @@
9
9
  * ~/.hq/anywhere/claude-install.json so uninstall can restore files byte-for-byte.
10
10
  */
11
11
  import * as fs from 'fs';
12
+ import * as os from 'node:os';
12
13
  import * as path from 'path';
13
14
  import { spawnSync } from 'node:child_process';
14
15
  import { buildCodexServerDef, claudeConfigFormat, codexConfigFormat, codexRemovalFormat, isCodexInstalled, jsonFormat, mergeCodexServer, removeCodexServer, registerCodexServer, unregisterCodexServer, mergeClaudeServer, readConfigDoc, realpathOrSelf, registerClaudeServer, resolveEnv, unregisterClaudeServer, writeConfigAtomic, } from './mcp-registration.js';
@@ -20,6 +21,8 @@ import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js'
20
21
  import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
21
22
  import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
22
23
  import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/registry/registry.js';
24
+ import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
25
+ import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
23
26
  import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
24
27
  import { readDaemonConfig } from '../lib/daemon/config.js';
25
28
  import { daemonPaths } from '../lib/daemon/paths.js';
@@ -201,9 +204,13 @@ function mcpManifest(opts) {
201
204
  }
202
205
  return { type: 'stdio', command, args: [entry, 'mcp', 'serve'], env: { PATH: MCP_SUBPROCESS_PATH } };
203
206
  }
207
+ function skillLinkName(name) {
208
+ return name.startsWith(SKILL_LINK_PREFIX) ? name : `${SKILL_LINK_PREFIX}${name}`;
209
+ }
204
210
  function planSkills(hqRoot, t) {
205
211
  const source = hqSkillsSource(hqRoot);
206
- const plan = { link: [], already: [], skipped: [] };
212
+ const plan = { link: [], already: [], skipped: [], unresolved: [], companyScoped: [] };
213
+ const companySlugs = new Set(Object.keys(readCompaniesManifestMap(hqRoot) ?? {}));
207
214
  let names;
208
215
  try {
209
216
  names = fs
@@ -218,8 +225,23 @@ function planSkills(hqRoot, t) {
218
225
  throw e;
219
226
  }
220
227
  for (const name of names) {
221
- const link = path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`);
228
+ const namespace = name.includes(':') ? name.slice(0, name.indexOf(':')) : '';
229
+ if (namespace && companySlugs.has(namespace)) {
230
+ plan.companyScoped.push(name);
231
+ continue;
232
+ }
222
233
  const target = path.join(source, name);
234
+ try {
235
+ if (!fs.statSync(target).isDirectory()) {
236
+ plan.unresolved.push(name);
237
+ continue;
238
+ }
239
+ }
240
+ catch {
241
+ plan.unresolved.push(name);
242
+ continue;
243
+ }
244
+ const link = path.join(t.skillsDir, skillLinkName(name));
223
245
  let st = null;
224
246
  try {
225
247
  st = fs.lstatSync(link);
@@ -237,6 +259,15 @@ function planSkills(hqRoot, t) {
237
259
  }
238
260
  return plan;
239
261
  }
262
+ function reportSkippedSkills(plan, log, dryRun) {
263
+ const prefix = dryRun ? 'Would skip' : 'Skipped';
264
+ if (plan.unresolved.length > 0) {
265
+ log(`${prefix} skill source(s) that do not resolve to directories: ${plan.unresolved.join(', ')}`);
266
+ }
267
+ if (plan.companyScoped.length > 0) {
268
+ log(`${prefix} company-scoped skill(s) at global scope: ${plan.companyScoped.join(', ')}`);
269
+ }
270
+ }
240
271
  function hqSkillLinksForRoot(skillsDir, hqRoot) {
241
272
  let names;
242
273
  try {
@@ -261,6 +292,27 @@ function hqSkillLinksForRoot(skillsDir, hqRoot) {
261
292
  }
262
293
  }).map((name) => path.join(skillsDir, name));
263
294
  }
295
+ function hqCompanySkillLinksForRoot(skillsDir, hqRoot) {
296
+ const companySlugs = new Set(Object.keys(readCompaniesManifestMap(hqRoot) ?? {}));
297
+ if (companySlugs.size === 0)
298
+ return [];
299
+ const source = hqSkillsSource(hqRoot);
300
+ return hqSkillLinksForRoot(skillsDir, hqRoot).filter((link) => {
301
+ try {
302
+ const target = path.resolve(skillsDir, fs.readlinkSync(link));
303
+ if (path.dirname(target) !== source)
304
+ return false;
305
+ const sourceName = path.basename(target);
306
+ const separator = sourceName.indexOf(':');
307
+ return separator > 0 && companySlugs.has(sourceName.slice(0, separator));
308
+ }
309
+ catch (error) {
310
+ if (error.code === 'ENOENT')
311
+ return false;
312
+ throw error;
313
+ }
314
+ });
315
+ }
264
316
  function removeSeeded(seeded, regFile) {
265
317
  if (seeded.length === 0 || !fs.existsSync(regFile))
266
318
  return;
@@ -320,7 +372,13 @@ export function installGlobalClaude(opts) {
320
372
  : mdOld === null ? null : (removeClaudeMdBlock(mdOld) || null))
321
373
  : prior ? prior.claudeMd.original : mdOld;
322
374
  const mdNew = pluginSource ? mdOriginal : addClaudeMdBlock(mdOld, hqRoot);
323
- const skills = pluginSource ? { link: [], already: [], skipped: [] } : planSkills(hqRoot, t);
375
+ const skills = pluginSource ? {
376
+ link: [],
377
+ already: [],
378
+ skipped: [],
379
+ unresolved: [],
380
+ companyScoped: [],
381
+ } : planSkills(hqRoot, t);
324
382
  const settingsOld = readOrNull(t.settingsJson);
325
383
  const settingsDoc = readConfigDoc(t.settingsJson, jsonFormat).doc;
326
384
  const directHooksRemoved = pluginSource
@@ -347,7 +405,9 @@ export function installGlobalClaude(opts) {
347
405
  const settingsNext = pluginSource
348
406
  ? cleanupRoots.reduce((doc, root) => removeHqHooks(doc, root), settingsDoc)
349
407
  : addHqHooks(settingsDoc, hqRoot, hookTarget);
350
- const skillLinksToRemove = pluginSource ? cleanupRoots.flatMap((root) => hqSkillLinksForRoot(t.skillsDir, root)) : [];
408
+ const skillLinksToRemove = [...new Set(pluginSource
409
+ ? cleanupRoots.flatMap((root) => hqSkillLinksForRoot(t.skillsDir, root))
410
+ : cleanupRoots.flatMap((root) => hqCompanySkillLinksForRoot(t.skillsDir, root)))];
351
411
  const claudeDoc = readConfigDoc(t.claudeJson, claudeConfigFormat).doc;
352
412
  // Collision check runs here too, so a dry run surfaces a conflicting "hq" server.
353
413
  const claudeNext = pluginSource ? claudeDoc : mergeClaudeServer(HQ_MCP_SERVER_NAME, {
@@ -359,7 +419,7 @@ export function installGlobalClaude(opts) {
359
419
  if (mdOld !== mdNew)
360
420
  touched.push(t.claudeMd);
361
421
  for (const s of skills.link)
362
- touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
422
+ touched.push(path.join(t.skillsDir, skillLinkName(s)));
363
423
  touched.push(...skillLinksToRemove);
364
424
  if (settingsNext !== settingsDoc)
365
425
  touched.push(t.settingsJson);
@@ -374,8 +434,12 @@ export function installGlobalClaude(opts) {
374
434
  for (const f of touched)
375
435
  log(` ${f}`);
376
436
  if (skills.skipped.length > 0) {
377
- log(`Would skip ${skills.skipped.length} skill(s) whose hq-<name> already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
437
+ log(`Would skip ${skills.skipped.length} skill(s) whose target already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
378
438
  }
439
+ if (!pluginSource && skillLinksToRemove.length > 0) {
440
+ log(`Would remove ${skillLinksToRemove.length} previous company-scoped skill link(s)`);
441
+ }
442
+ reportSkippedSkills(skills, log, true);
379
443
  log(`\n--- ${t.settingsJson}`);
380
444
  log(lineDiff(jsonFormat.serialize(settingsDoc), jsonFormat.serialize(settingsNext)));
381
445
  log(`\n--- ${t.claudeJson}`);
@@ -429,11 +493,10 @@ export function installGlobalClaude(opts) {
429
493
  if (!pluginSource)
430
494
  fs.mkdirSync(t.skillsDir, { recursive: true });
431
495
  for (const name of skills.link) {
432
- fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
496
+ fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, skillLinkName(name)));
433
497
  }
434
- if (pluginSource)
435
- for (const link of skillLinksToRemove)
436
- fs.unlinkSync(link);
498
+ for (const link of skillLinksToRemove)
499
+ fs.unlinkSync(link);
437
500
  if (pluginSource) {
438
501
  // Migrate a prior direct install: remove only our old hook/MCP entries while
439
502
  // retaining the original user settings for a later uninstall restore.
@@ -498,6 +561,7 @@ export function installGlobalClaude(opts) {
498
561
  writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
499
562
  log(`HQ installed for Claude Code from ${hqRoot}.`);
500
563
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
564
+ reportSkippedSkills(skills, log, false);
501
565
  if (pluginSource)
502
566
  log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
503
567
  else {
@@ -691,6 +755,7 @@ export function installGlobalCodex(opts) {
691
755
  const charter = fs.readFileSync(codexCharterSource(hqRoot), 'utf-8');
692
756
  const mdOld = readOrNull(t.agentsMd);
693
757
  const mdNew = addAgentsMdBlock(mdOld, charter);
758
+ const cleanupRoots = [...new Set([hqRoot, ...(prior?.hqRoot ? [prior.hqRoot] : [])])];
694
759
  const legacy = classifyLegacySkills(t.skillsDir, hqRoot);
695
760
  const legacyPath = path.join(t.skillsDir, LEGACY_SKILLS_NAME);
696
761
  const legacyBackupPath = path.join(t.skillsDir, LEGACY_BACKUP_NAME);
@@ -698,6 +763,7 @@ export function installGlobalCodex(opts) {
698
763
  throw new Error(`${legacyBackupPath} already exists; move it away before installing.`);
699
764
  }
700
765
  const skills = planSkills(hqRoot, t);
766
+ const companySkillLinksToRemove = [...new Set(cleanupRoots.flatMap((root) => hqCompanySkillLinksForRoot(t.skillsDir, root)))];
701
767
  const hooksOld = readOrNull(t.hooksJson);
702
768
  const hooksDoc = readConfigDoc(t.hooksJson, jsonFormat).doc;
703
769
  const hookTarget = hookTargetFor(hqRoot, home, opts.daemonInstalled, env);
@@ -719,7 +785,8 @@ export function installGlobalCodex(opts) {
719
785
  if (legacy.kind === 'hq-symlink' || legacy.kind === 'hq-dir')
720
786
  touched.push(path.join(t.skillsDir, LEGACY_SKILLS_NAME));
721
787
  for (const s of skills.link)
722
- touched.push(path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${s}`));
788
+ touched.push(path.join(t.skillsDir, skillLinkName(s)));
789
+ touched.push(...companySkillLinksToRemove);
723
790
  if (hooksNext !== hooksDoc)
724
791
  touched.push(t.hooksJson);
725
792
  if (tomlNext !== tomlDoc || legacyPackServerPresent)
@@ -736,8 +803,12 @@ export function installGlobalCodex(opts) {
736
803
  else if (legacy.kind === 'foreign')
737
804
  log(legacyWarning(t.skillsDir));
738
805
  if (skills.skipped.length > 0) {
739
- log(`Would skip ${skills.skipped.length} skill(s) whose hq-<name> already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
806
+ log(`Would skip ${skills.skipped.length} skill(s) whose target already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
807
+ }
808
+ if (companySkillLinksToRemove.length > 0) {
809
+ log(`Would remove ${companySkillLinksToRemove.length} previous company-scoped skill link(s)`);
740
810
  }
811
+ reportSkippedSkills(skills, log, true);
741
812
  log(`\n--- ${t.hooksJson}`);
742
813
  log(lineDiff(jsonFormat.serialize(hooksDoc), jsonFormat.serialize(hooksNext)));
743
814
  log(`\n--- ${t.configToml}`);
@@ -761,8 +832,10 @@ export function installGlobalCodex(opts) {
761
832
  log(legacyWarning(t.skillsDir));
762
833
  fs.mkdirSync(t.skillsDir, { recursive: true });
763
834
  for (const name of skills.link) {
764
- fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, `${SKILL_LINK_PREFIX}${name}`));
835
+ fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, skillLinkName(name)));
765
836
  }
837
+ for (const link of companySkillLinksToRemove)
838
+ fs.unlinkSync(link);
766
839
  writeConfigAtomic({
767
840
  target: t.hooksJson,
768
841
  pack: HQ_ANYWHERE_PACK,
@@ -803,6 +876,7 @@ export function installGlobalCodex(opts) {
803
876
  writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
804
877
  log(`HQ installed for Codex from ${hqRoot}.`);
805
878
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
879
+ reportSkippedSkills(skills, log, false);
806
880
  log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
807
881
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
808
882
  return touched;
@@ -946,34 +1020,63 @@ function assertRuntime(runtime) {
946
1020
  }
947
1021
  return r;
948
1022
  }
949
- export function resolveInstallRoot(explicit) {
950
- const fromFlag = explicit?.trim() || process.env.HQ_ROOT?.trim();
951
- const root = fromFlag ? path.resolve(fromFlag) : resolveHqRoot();
952
- if (!root) {
1023
+ export function resolveInstallRoot(explicit, options = {}) {
1024
+ const env = options.env ?? process.env;
1025
+ const fromFlag = explicit?.trim() || env.HQ_ROOT?.trim();
1026
+ if (fromFlag)
1027
+ return path.resolve(fromFlag);
1028
+ const cwdRoot = resolveHqRoot(options.cwd ?? process.cwd());
1029
+ if (cwdRoot)
1030
+ return cwdRoot;
1031
+ const home = options.home?.trim() || env.HOME?.trim() || os.homedir();
1032
+ try {
1033
+ const pointer = fs.readFileSync(path.join(home, '.hq', 'root'), 'utf8').trim();
1034
+ if (pointer) {
1035
+ const pointedRoot = resolveHqRoot(pointer);
1036
+ if (pointedRoot)
1037
+ return pointedRoot;
1038
+ }
1039
+ }
1040
+ catch {
1041
+ // Fall through to the regular installer/config root discovery.
1042
+ }
1043
+ try {
1044
+ const rootEnv = { ...env };
1045
+ delete rootEnv.HQ_ROOT;
1046
+ return resolveDefaultHqRoot({
1047
+ onMissing: 'throw',
1048
+ env: rootEnv,
1049
+ cwd: options.cwd,
1050
+ home,
1051
+ });
1052
+ }
1053
+ catch {
953
1054
  throw Object.assign(new Error('Run from inside your HQ checkout, or pass --hq-root <dir>.'), { expected: true });
954
1055
  }
955
- return root;
956
1056
  }
957
1057
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
958
1058
  export async function runInstallGlobal(opts) {
959
1059
  const runtime = assertRuntime(opts.runtime);
960
1060
  const env = opts.env ?? process.env;
961
- if (runtime === 'chatgpt' || runtime === 'grok') {
1061
+ if (runtime === 'chatgpt' || runtime === 'grok' || opts.dryRun) {
962
1062
  const enabled = await anywhereRuntimeAllowed(env, {
963
1063
  ...(opts.personSettingReader ? { readSetting: opts.personSettingReader } : {}),
964
1064
  onError: (name) => (opts.log ?? console.log)(`HQ Anywhere person setting unavailable (${name}); access remains off.`),
965
1065
  });
966
1066
  if (!enabled)
967
1067
  throw new AnywhereRuntimeDisabledError();
968
- const endpoint = resolveHqMcpEndpoint(opts.env ?? process.env).toString();
969
- const lines = runtime === 'chatgpt' ? chatgptConnectorInstructions(endpoint) : grokConnectorInstructions(endpoint);
970
- for (const line of lines)
971
- (opts.log ?? console.log)(line);
972
- return;
1068
+ if (runtime === 'chatgpt' || runtime === 'grok') {
1069
+ const endpoint = resolveHqMcpEndpoint(env).toString();
1070
+ const lines = runtime === 'chatgpt' ? chatgptConnectorInstructions(endpoint) : grokConnectorInstructions(endpoint);
1071
+ for (const line of lines)
1072
+ (opts.log ?? console.log)(line);
1073
+ return;
1074
+ }
973
1075
  }
974
1076
  const install = runtime === 'codex' ? installGlobalCodex : installGlobalClaude;
975
1077
  install({
976
- hqRoot: resolveInstallRoot(opts.hqRoot),
1078
+ hqRoot: resolveInstallRoot(opts.hqRoot, { env, cwd: opts.cwd, home: opts.home }),
1079
+ home: opts.home,
977
1080
  dryRun: opts.dryRun,
978
1081
  via: opts.via,
979
1082
  marketplaceDir: opts.marketplaceDir,
@@ -22,6 +22,7 @@ export declare function mappingIncludesStory(mapping: ProjectMapping, storyId: s
22
22
  export interface ProjectReadResult {
23
23
  stories: Array<Record<string, unknown>>;
24
24
  mtime: number;
25
+ focus_overrides?: unknown;
25
26
  errors?: Array<{
26
27
  path: string;
27
28
  field: string;
@@ -272,7 +272,12 @@ export function readProject(root, company, prdPath, tolerantStories = false) {
272
272
  return [story];
273
273
  });
274
274
  try {
275
- return { stories, mtime: fs.statSync(prdPath).mtimeMs, ...(tolerantStories ? { errors } : {}) };
275
+ return {
276
+ stories,
277
+ mtime: fs.statSync(prdPath).mtimeMs,
278
+ ...(parsed.focus_overrides === undefined ? {} : { focus_overrides: parsed.focus_overrides }),
279
+ ...(tolerantStories ? { errors } : {}),
280
+ };
276
281
  }
277
282
  catch (err) {
278
283
  return fail("tracker_checks_input_unreadable", `Project task list could not be stat'ed: ${err instanceof Error ? err.message : String(err)}`, { path: prdPath });
@@ -738,6 +738,7 @@ export function createLaneMessageHandlers(deps) {
738
738
  ...(automatic.preservedModel !== undefined ? { preservedModel: automatic.preservedModel } : {}),
739
739
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
740
740
  ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "balanced" } : {}),
741
+ ...(automatic.historyReason === "profile_pin_exhausted" ? { preserveAdmissionAccounts: true } : {}),
741
742
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
742
743
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
743
744
  pending: {
@@ -1,10 +1,11 @@
1
1
  import type { Command } from "commander";
2
2
  import { type QuestionBody } from "../lib/lanes/questions.js";
3
- import type { QuestionRecord } from "../lib/lanes/types.js";
3
+ import type { LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
4
4
  import { type LaneChecksDeps } from "./lanes-checks.js";
5
5
  type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
6
6
  export interface LaneReportsDeps extends LaneChecksDeps {
7
7
  readQuestionBody: (root: string, question: QuestionRecord) => QuestionBody;
8
+ focusDashboardEnabled: (company: LaneCompanyRef) => Promise<boolean>;
8
9
  }
9
10
  export declare function registerLaneReportCommands(parent: Command, deps: LaneReportsDeps, wrap: WrapHandler): void;
10
11
  export {};
@@ -163,7 +163,7 @@ function issueFromError(err, fallbackPath, fallbackField, ids = {}) {
163
163
  ...(typeof details.question_id === "string" ? { question_id: details.question_id } : ids.question_id ? { question_id: ids.question_id } : {}),
164
164
  };
165
165
  }
166
- async function teamInput(root, company, project, questions, deps, errors = []) {
166
+ async function teamInput(root, company, project, questions, deps, errors = [], includeFocusDashboard = false) {
167
167
  const projectDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
168
168
  const prdPath = path.join(projectDirectory, "prd.json");
169
169
  let prd = { stories: [], mtime: 0 };
@@ -213,10 +213,64 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
213
213
  }
214
214
  normalizedUpdatedAt = normalized;
215
215
  }
216
+ const ownerValue = includeFocusDashboard ? story.owner ?? story.assignee : undefined;
217
+ let owner = null;
218
+ if (ownerValue !== undefined && ownerValue !== null) {
219
+ if (typeof ownerValue === "string" && ownerValue.trim())
220
+ owner = ownerValue.trim();
221
+ else
222
+ errors.push({ path: prdPath, field: "owner", story_id: id, message: "Story owner must be a non-empty string when present" });
223
+ }
224
+ const deadlineValue = includeFocusDashboard ? story.deadline ?? story.due_at ?? story.due_date : undefined;
225
+ let deadline = null;
226
+ if (deadlineValue !== undefined && deadlineValue !== null) {
227
+ if (typeof deadlineValue === "string" && deadlineValue.trim())
228
+ deadline = deadlineValue.trim();
229
+ else
230
+ errors.push({ path: prdPath, field: "deadline", story_id: id, message: "Story deadline must be a non-empty string when present" });
231
+ }
216
232
  const refs = storyReferences(story, prdPath, id, errors);
217
233
  pullRequests.push(...refs.pullRequests);
218
234
  releases.push(...refs.releases);
219
- stories.push({ id, title, status, references: refs.references, ...(normalizedUpdatedAt ? { updated_at: normalizedUpdatedAt } : {}), source_path: prdPath });
235
+ stories.push({
236
+ id,
237
+ title,
238
+ status,
239
+ focus_id: story.focus_id,
240
+ kpi: story.kpi,
241
+ owner,
242
+ deadline,
243
+ references: refs.references,
244
+ ...(normalizedUpdatedAt ? { updated_at: normalizedUpdatedAt } : {}),
245
+ source_path: prdPath,
246
+ });
247
+ }
248
+ const focusOverrides = [];
249
+ if (includeFocusDashboard && prd.focus_overrides !== undefined) {
250
+ if (!Array.isArray(prd.focus_overrides)) {
251
+ errors.push({ path: prdPath, field: "focus_overrides", message: "Project focus_overrides must be an array" });
252
+ }
253
+ else {
254
+ for (const [index, raw] of prd.focus_overrides.entries()) {
255
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
256
+ errors.push({ path: prdPath, field: `focus_overrides[${index}]`, message: "Focus override must be an object" });
257
+ continue;
258
+ }
259
+ const record = raw;
260
+ if (["reason", "by_lane", "at", "story"].some((field) => typeof record[field] !== "string" || !record[field].trim())) {
261
+ errors.push({ path: prdPath, field: `focus_overrides[${index}]`, message: "Focus override needs non-empty reason, by_lane, at, and story fields" });
262
+ continue;
263
+ }
264
+ const at = normalizeIsoTimestamp(record.at);
265
+ if (at === null) {
266
+ errors.push({ path: prdPath, field: `focus_overrides[${index}].at`, message: "Focus override at must be a valid timestamp" });
267
+ continue;
268
+ }
269
+ const override = { reason: record.reason, by_lane: record.by_lane, at, story: record.story };
270
+ if (mappingIncludesStory(project, override.story))
271
+ focusOverrides.push(override);
272
+ }
273
+ }
220
274
  }
221
275
  const teamQuestions = [];
222
276
  for (const record of questions.filter((question) => question.project_id === project.projectId
@@ -236,6 +290,7 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
236
290
  team: project.team,
237
291
  project_id: project.projectId,
238
292
  stories,
293
+ focus_overrides: focusOverrides,
239
294
  pull_requests: pullRequests,
240
295
  releases,
241
296
  blockers: await trackerRecords(root, company, project, "blocker", errors),
@@ -246,7 +301,7 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
246
301
  errors,
247
302
  };
248
303
  }
249
- async function allTeamInputs(root, company, deps) {
304
+ async function allTeamInputs(root, company, deps, includeFocusDashboard) {
250
305
  const mappings = loadProjectMappings(root, company.slug);
251
306
  const teams = [];
252
307
  for (const project of [...mappings.values()].sort((a, b) => a.team.localeCompare(b.team) || a.projectId.localeCompare(b.projectId))) {
@@ -258,7 +313,7 @@ async function allTeamInputs(root, company, deps) {
258
313
  catch (err) {
259
314
  errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", project.projectId), "questions"));
260
315
  }
261
- teams.push(await teamInput(root, company, project, questions, deps, errors));
316
+ teams.push(await teamInput(root, company, project, questions, deps, errors, includeFocusDashboard));
262
317
  }
263
318
  return teams;
264
319
  }
@@ -282,6 +337,7 @@ export function registerLaneReportCommands(parent, deps, wrap) {
282
337
  const company = await resolveTrackerCompany(deps, options.company);
283
338
  const root = deps.hqRoot();
284
339
  const generatedAt = deps.now().toISOString();
340
+ const focusDashboardEnabled = await deps.focusDashboardEnabled(company);
285
341
  const previous = options.since === undefined ? readTrackerReportState(root, company.slug) : null;
286
342
  const since = options.since ?? previous?.last_report_at ?? FIRST_TRACKER_REPORT_SINCE;
287
343
  const input = {
@@ -289,7 +345,8 @@ export function registerLaneReportCommands(parent, deps, wrap) {
289
345
  generated_at: generatedAt,
290
346
  since,
291
347
  since_source: options.since !== undefined ? "explicit" : previous ? "previous_report" : "first_run",
292
- teams: await allTeamInputs(root, company, deps),
348
+ focus_dashboard_enabled: focusDashboardEnabled,
349
+ teams: await allTeamInputs(root, company, deps, focusDashboardEnabled),
293
350
  };
294
351
  const report = buildTrackerOwnerReport(input);
295
352
  const hasErrors = report.teams.some((team) => team.errors.length > 0);
@@ -323,7 +380,7 @@ export function registerLaneReportCommands(parent, deps, wrap) {
323
380
  catch (err) {
324
381
  errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", candidates[0].projectId), "questions"));
325
382
  }
326
- const input = await teamInput(root, company, candidates[0], questions, deps, errors);
383
+ const input = await teamInput(root, company, candidates[0], questions, deps, errors, false);
327
384
  const page = buildTrackerTeamPage(input, company.slug, since, generatedAt);
328
385
  if (page.errors.length > 0)
329
386
  process.exitCode = 2;
@@ -409,6 +409,9 @@ export function createLaneResumeHandlers(deps) {
409
409
  : {}),
410
410
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
411
411
  ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "balanced" } : {}),
412
+ ...(automaticAccountSelection.historyReason === "profile_pin_exhausted"
413
+ ? { preserveAdmissionAccounts: true }
414
+ : {}),
412
415
  ...(profileRepin !== undefined ? { profileRepin } : {}),
413
416
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
414
417
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),