@indigoai-us/hq-cli 5.345.148 → 5.345.150

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 (83) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +2 -0
  3. package/dist/command-catalog.generated.d.ts +62 -14
  4. package/dist/command-catalog.generated.js +74 -14
  5. package/dist/command-help.js +2 -1
  6. package/dist/commands/_patterns.d.ts +6 -0
  7. package/dist/commands/_patterns.js +6 -0
  8. package/dist/commands/access.d.ts +45 -2
  9. package/dist/commands/access.js +537 -223
  10. package/dist/commands/billing-usage.d.ts +33 -2
  11. package/dist/commands/billing-usage.js +75 -4
  12. package/dist/commands/bot.js +2 -2
  13. package/dist/commands/channels.d.ts +27 -0
  14. package/dist/commands/channels.js +108 -4
  15. package/dist/commands/core-refresh-vault-access-native.d.ts +16 -0
  16. package/dist/commands/core-refresh-vault-access-native.js +142 -9
  17. package/dist/commands/core.js +4 -1
  18. package/dist/commands/dm.js +9 -1
  19. package/dist/commands/groups.js +9 -6
  20. package/dist/commands/install-global.d.ts +1 -8
  21. package/dist/commands/install-global.js +11 -27
  22. package/dist/commands/lanes-questions.js +7 -4
  23. package/dist/commands/lanes.js +13 -6
  24. package/dist/commands/library-add.d.ts +31 -1
  25. package/dist/commands/library-add.js +69 -2
  26. package/dist/commands/library.js +37 -3
  27. package/dist/commands/members.d.ts +35 -0
  28. package/dist/commands/members.js +65 -14
  29. package/dist/commands/reindex.js +19 -0
  30. package/dist/commands/secrets.d.ts +5 -0
  31. package/dist/commands/secrets.js +11 -6
  32. package/dist/hook-engine-path.js +10 -3
  33. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +63282 -62480
  34. package/dist/hq-cloud-sync-runner/package.json +1 -1
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.js +70 -5
  37. package/dist/lazy-commands.js +2 -0
  38. package/dist/lib/billing/usage-cost.d.ts +31 -1
  39. package/dist/lib/billing/usage-cost.js +46 -0
  40. package/dist/lib/db/local.d.ts +2 -2
  41. package/dist/lib/db/local.js +2 -2
  42. package/dist/lib/db/sql.js +2 -2
  43. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  44. package/dist/lib/db/sqlite-binding.js +168 -0
  45. package/dist/lib/doctor/checks/anywhere.d.ts +12 -0
  46. package/dist/lib/doctor/checks/anywhere.js +63 -12
  47. package/dist/lib/doctor/checks/cli-path.d.ts +2 -1
  48. package/dist/lib/doctor/checks/cli-path.js +2 -1
  49. package/dist/lib/doctor/checks/hook-engine.js +2 -1
  50. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  51. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  52. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  53. package/dist/lib/hook-engine/reconcile.js +134 -0
  54. package/dist/lib/hook-engine/registration.d.ts +17 -0
  55. package/dist/lib/hook-engine/registration.js +50 -0
  56. package/dist/lib/install/claude-targets.d.ts +2 -1
  57. package/dist/lib/install/claude-targets.js +8 -3
  58. package/dist/lib/install/codex-targets.d.ts +3 -0
  59. package/dist/lib/install/codex-targets.js +15 -4
  60. package/dist/lib/install/hook-target.d.ts +9 -0
  61. package/dist/lib/install/hook-target.js +39 -0
  62. package/dist/lib/lanes/index.d.ts +1 -1
  63. package/dist/lib/lanes/index.js +1 -1
  64. package/dist/lib/lanes/store.d.ts +11 -0
  65. package/dist/lib/lanes/store.js +20 -0
  66. package/dist/lib/lanes/watch.js +5 -3
  67. package/dist/lib/windows/console-encoding.d.ts +24 -0
  68. package/dist/lib/windows/console-encoding.js +83 -0
  69. package/dist/lib/windows/install-followup.d.ts +35 -0
  70. package/dist/lib/windows/install-followup.js +142 -0
  71. package/dist/lib/windows/user-path.d.ts +80 -3
  72. package/dist/lib/windows/user-path.js +187 -26
  73. package/dist/main.js +2 -1
  74. package/dist/unknown-command.d.ts +18 -0
  75. package/dist/unknown-command.js +88 -0
  76. package/dist/utils/cognito-session.d.ts +8 -0
  77. package/dist/utils/cognito-session.js +14 -2
  78. package/dist/utils/hook-trust.d.ts +9 -0
  79. package/dist/utils/hook-trust.js +30 -2
  80. package/dist/utils/qmd-native-binding-error.js +3 -1
  81. package/dist/utils/resolve-executable.d.ts +2 -0
  82. package/dist/utils/resolve-executable.js +6 -2
  83. package/package.json +9 -9
@@ -2,7 +2,6 @@ import { Command } from 'commander';
2
2
  import { AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
3
3
  import { type RuntimeHookTrustResult } from '../utils/hook-trust.js';
4
4
  import { type ShimPathCheck } from '../lib/windows/user-path.js';
5
- import { type HookTarget } from '../lib/install/claude-targets.js';
6
5
  export interface GlobalInstallOptions {
7
6
  /** HQ checkout to install from (absolute). */
8
7
  hqRoot: string;
@@ -32,13 +31,7 @@ export interface GlobalInstallOptions {
32
31
  /** Platform override for link creation (default process.platform). */
33
32
  platform?: NodeJS.Platform;
34
33
  }
35
- /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
36
- export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
37
- /**
38
- * US-010: with hqd installed (and the checkout shipping the shim), hooks go to
39
- * the hqd shim; otherwise to master-hook.sh / the Codex adapter.
40
- */
41
- export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
34
+ export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
42
35
  export interface ClaudePluginSource {
43
36
  sourceDir: string;
44
37
  marketplaceName: string;
@@ -1,3 +1,4 @@
1
+ import { hookTargetFor } from '../lib/install/hook-target.js';
1
2
  /**
2
3
  * `hq install --global --runtime claude` / `hq uninstall --global --runtime claude`
3
4
  * (hq-anywhere-runtime US-005).
@@ -25,34 +26,11 @@ import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/r
25
26
  import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
26
27
  import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
27
28
  import { trustCodexUserHooks } from '../utils/hook-trust.js';
28
- import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
29
- import { readDaemonConfig } from '../lib/daemon/config.js';
30
- import { daemonPaths } from '../lib/daemon/paths.js';
31
29
  import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
32
- import { checkShimOnPath, shimPathWarning } from '../lib/windows/user-path.js';
30
+ import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
33
31
  import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
34
32
  import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
35
- /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
36
- export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
37
- if (platform === 'win32')
38
- return queryTask(DAEMON_WINDOWS_TASK);
39
- const candidates = platform === 'darwin'
40
- ? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
41
- : platform === 'linux'
42
- ? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
43
- : [];
44
- return candidates.some((p) => fs.existsSync(p));
45
- }
46
- /**
47
- * US-010: with hqd installed (and the checkout shipping the shim), hooks go to
48
- * the hqd shim; otherwise to master-hook.sh / the Codex adapter.
49
- */
50
- export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
51
- const installed = daemonInstalled ?? isDaemonInstalled(home);
52
- const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
53
- const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
54
- return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
55
- }
33
+ export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
56
34
  const HQ_PACKAGES_MARKETPLACE_REPOSITORY = 'indigoai-us/hq-packages';
57
35
  /** Resolve local overrides first, then the published HQ marketplace. */
58
36
  export function resolveClaudePluginSource(explicitDir, env = process.env, repository = HQ_PACKAGES_MARKETPLACE_REPOSITORY) {
@@ -615,7 +593,7 @@ export function installGlobalClaude(opts) {
615
593
  if (pluginSource)
616
594
  log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
617
595
  else {
618
- log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
596
+ log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
619
597
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
620
598
  }
621
599
  log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
@@ -994,7 +972,7 @@ export function installGlobalCodex(opts) {
994
972
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
995
973
  reportSkillCopies(linkKinds, log);
996
974
  reportSkippedSkills(skills, log, false);
997
- log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
975
+ log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
998
976
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
999
977
  return touched;
1000
978
  }
@@ -1213,6 +1191,12 @@ export async function runInstallGlobal(opts) {
1213
1191
  if (runtime === 'codex' && !opts.dryRun)
1214
1192
  await trustInstalledCodexHooks(hqRoot, opts);
1215
1193
  if (!opts.dryRun) {
1194
+ try {
1195
+ ensureShimOnUserPath({ env, entry: process.argv[1] });
1196
+ }
1197
+ catch {
1198
+ // The warning below is the fallback when the user Path cannot be updated.
1199
+ }
1216
1200
  const shimPath = (opts.shimPathCheck ?? (() => checkShimOnPath({ env })))();
1217
1201
  if (shimPath)
1218
1202
  for (const line of shimPathWarning(shimPath))
@@ -2,7 +2,7 @@ import * as path from "node:path";
2
2
  import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
3
3
  import { assertNoSecretText } from "../lib/lanes/text-scan.js";
4
4
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
5
- import { appendCappedHistory, createQuestion, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
5
+ import { appendCappedHistory, createQuestion, expandLocalLaneIdPrefix, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
6
6
  import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
7
7
  import { LanesError } from "../lib/lanes/errors.js";
8
8
  import { applyLoopStallAnswer, findLoopStallResult } from "../lib/lanes/loop.js";
@@ -565,6 +565,9 @@ export function createLaneQuestionsHandlers(deps) {
565
565
  const hqRoot = ctx.hqRoot();
566
566
  const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
567
567
  const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
568
+ // --from matches the full lane id; a short prefix expands only when it
569
+ // names exactly one local lane (ambiguous prefixes are refused).
570
+ const from = opts.from ? (expandLocalLaneIdPrefix(hqRoot, opts.from) ?? opts.from) : undefined;
568
571
  const company = opts.company
569
572
  ? await ctx.resolveCompany(opts.company)
570
573
  : await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
@@ -574,7 +577,7 @@ export function createLaneQuestionsHandlers(deps) {
574
577
  .filter((q) => {
575
578
  if (status && q.status !== status)
576
579
  return false;
577
- if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
580
+ if (from && q.lane_id !== from && q.origin.id !== from)
578
581
  return false;
579
582
  if (opts.project && q.project_id !== opts.project)
580
583
  return false;
@@ -618,7 +621,7 @@ export function createLaneQuestionsHandlers(deps) {
618
621
  ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company, undefined, {
619
622
  sessionId: currentSessionId ?? undefined,
620
623
  for: opts.for,
621
- from: opts.from,
624
+ from,
622
625
  project: opts.project,
623
626
  story: opts.story,
624
627
  status: opts.status,
@@ -651,7 +654,7 @@ export function createLaneQuestionsHandlers(deps) {
651
654
  .filter(({ question: q, lane, audience }) => {
652
655
  if (status && q.status !== status)
653
656
  return false;
654
- if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
657
+ if (from && q.lane_id !== from && q.origin.id !== from)
655
658
  return false;
656
659
  if (opts.project && q.project_id !== opts.project)
657
660
  return false;
@@ -78,7 +78,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
78
78
  import { listLaneReviews } from "../lib/lanes/reviews.js";
79
79
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
80
80
  import { remoteControlSelectionConfig, remoteControlSessionHome } from "../lib/lanes/remote-control-lane.js";
81
- import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
81
+ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, expandLocalLaneIdPrefix, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
82
82
  import { admitQueuedNow } from "../lib/lanes/reconcile.js";
83
83
  import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
84
84
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
@@ -1979,6 +1979,11 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1979
1979
  if (localMatch)
1980
1980
  return { lane: readLane(ctx.hqRoot(), localMatch), remoteOnly: false };
1981
1981
  }
1982
+ // A short prefix expands only to exactly one local lane; more than one is
1983
+ // refused with the candidates listed (lane_id_ambiguous).
1984
+ const prefixMatch = expandLocalLaneIdPrefix(ctx.hqRoot(), laneId);
1985
+ if (prefixMatch)
1986
+ return { lane: readLane(ctx.hqRoot(), prefixMatch), remoteOnly: false };
1982
1987
  const separator = laneId.lastIndexOf("_");
1983
1988
  const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
1984
1989
  const ownerLookupGate = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
@@ -4561,7 +4566,8 @@ async function runCreate(ctx, opts) {
4561
4566
  printJson({
4562
4567
  ok: true,
4563
4568
  action: "create",
4564
- admitted,
4569
+ // lane_id stays ahead of `admitted`: callers that take the first lane id
4570
+ // in this output must get the lane they created, not one it admitted.
4565
4571
  lane_id: started.lane_id,
4566
4572
  state: started.state,
4567
4573
  queued_reason: started.queued_reason ?? null,
@@ -4603,6 +4609,7 @@ async function runCreate(ctx, opts) {
4603
4609
  .outpost_command_id,
4604
4610
  }
4605
4611
  : {}),
4612
+ admitted,
4606
4613
  lane: started,
4607
4614
  });
4608
4615
  return started;
@@ -8942,10 +8949,10 @@ async function runCreateSubagent(ctx, opts) {
8942
8949
  }
8943
8950
  assertNoSecretText("focus override reason", opts.focusOverride.trim());
8944
8951
  }
8952
+ // A subagent lane is a board record for work the senior session runs with
8953
+ // its own host's subagent tool (Claude's Agent, Codex's spawn_agent). No
8954
+ // process is spawned, so any provider is only attribution.
8945
8955
  const provider = opts.provider ? parseLaneProvider(opts.provider) : "claude";
8946
- if (provider !== "claude") {
8947
- throw new LanesError("subagent_runner_requires_claude", "--runner subagent records a Claude subagent; use --provider claude or omit it", { provider });
8948
- }
8949
8956
  if (ctx.env.HQ_LANE_ID?.trim()) {
8950
8957
  throw new LanesError("subagent_runner_requires_session", "A subagent lane belongs to the session that runs the subagent; a lane worker cannot register one", { caller_lane_id: ctx.env.HQ_LANE_ID.trim() });
8951
8958
  }
@@ -10277,7 +10284,7 @@ export function registerLanesCommand(parent, deps = {}) {
10277
10284
  .option("--fresh-thread", "Skip worker continuity and start a cold provider session")
10278
10285
  .option("--loop", "Keep one worker lane registered to process queued phase envelopes")
10279
10286
  .option("--provider <name>", "claude | codex | grok")
10280
- .option("--runner <kind>", "process (default) | subagent: register a lane an in-session Claude subagent runs; skips admission and starts no process")
10287
+ .option("--runner <kind>", "process (default) | subagent: register a lane an in-session subagent runs (Claude Agent, Codex spawn_agent); skips admission and starts no process")
10281
10288
  .option("--remote-control", "Open a Claude lane in Remote Control using account a for login and the selected account for inference")
10282
10289
  .option("--title <text>", "Human-readable Claude Remote Control session title; required with --remote-control")
10283
10290
  .option("--account <id>", "Provider account id, or 'auto' to pick the account with the most room (all accounts are one pool across companies)")
@@ -1,5 +1,5 @@
1
1
  import { Readable } from "node:stream";
2
- import { type CloudAssetMediaKind } from "@indigoai-us/hq-cloud";
2
+ import { type CloudAssetMediaKind, type PullScope, type PullScopeClient } from "@indigoai-us/hq-cloud";
3
3
  import type { LibraryCaptionNote, LibraryCaptionRequest } from "./library-caption.js";
4
4
  export interface LibraryCompany {
5
5
  uid: string;
@@ -37,6 +37,13 @@ export interface LibraryAddDeps {
37
37
  * a caption failure never fails the upload. Absent = no captioning.
38
38
  */
39
39
  caption?: (request: LibraryCaptionRequest) => Promise<LibraryCaptionNote>;
40
+ /**
41
+ * The membership's sync scope for the company. Throws when it cannot be
42
+ * read. Absent = the scope is not checked and nothing is pinned.
43
+ */
44
+ syncScope?: (company: LibraryCompany) => Promise<Pick<PullScope, "syncMode" | "prefixSet">>;
45
+ /** `hq files get`'s pin writer (`.hq/pins.json`). */
46
+ addPin?: (hqRoot: string, companySlug: string, prefix: string) => void;
40
47
  }
41
48
  export interface LibraryAddOptions {
42
49
  company?: string;
@@ -60,9 +67,19 @@ export interface LibraryAddItem {
60
67
  /** AI caption outcome, set when captioning ran for this item. */
61
68
  caption?: LibraryCaptionNote;
62
69
  }
70
+ export interface LibrarySyncScopeNote {
71
+ status: "in-scope" | "pinned" | "not-pinned";
72
+ /** Company-relative pins written (`pinned`) or still needed (`not-pinned`). */
73
+ pins: string[];
74
+ /** Set when not pinned and `hq files get` can pin by hand (the library folder). */
75
+ command?: string;
76
+ reason?: string;
77
+ }
63
78
  export interface LibraryAddResult {
64
79
  company: string;
65
80
  items: LibraryAddItem[];
81
+ /** Set when at least one placeholder was written and the scope was checked. */
82
+ syncScope?: LibrarySyncScopeNote;
66
83
  }
67
84
  export declare function contentTypeFor(name: string, served?: string): string;
68
85
  export declare function mediaKindFor(contentType: string): CloudAssetMediaKind;
@@ -89,6 +106,19 @@ export declare function offloadTimestamp(now: Date): string;
89
106
  * only that source.
90
107
  */
91
108
  export declare function runLibraryAdd(inputs: readonly string[], options: LibraryAddOptions, deps: LibraryAddDeps): Promise<LibraryAddResult>;
109
+ /**
110
+ * A `shared` or `custom` sync pushes only paths inside the membership's
111
+ * prefix set (explicit grants plus pins), so a placeholder written outside it
112
+ * never reaches the vault. Pin it with `hq files get`'s pin writer: the
113
+ * default library folder as a whole, and for any other `--to` folder only the
114
+ * written placeholders, so the rest of that folder stays out of sync.
115
+ */
116
+ export declare function ensureLibraryInSyncScope(company: LibraryCompany, dirRel: string, placeholderPaths: readonly string[], deps: Pick<LibraryAddDeps, "hqRoot" | "syncScope" | "addPin">): Promise<LibrarySyncScopeNote | undefined>;
117
+ /**
118
+ * {@link resolvePullScope} answers `all` when any call fails. Report the
119
+ * failure instead, so a failed lookup is not mistaken for full sync.
120
+ */
121
+ export declare function resolveLibrarySyncScope(client: Required<PullScopeClient>, company: LibraryCompany, hqRoot: string): Promise<PullScope>;
92
122
  export interface LibraryTagResult {
93
123
  placeholderPath: string;
94
124
  tags: string[];
@@ -28,7 +28,7 @@ import http from "node:http";
28
28
  import https from "node:https";
29
29
  import path from "node:path";
30
30
  import { Readable } from "node:stream";
31
- import { formatCloudAssetBlock, formatCloudPlaceholder, parseCloudPlaceholder, } from "@indigoai-us/hq-cloud";
31
+ import { formatCloudAssetBlock, formatCloudPlaceholder, isCoveredByAny, parseCloudPlaceholder, resolvePullScope, } from "@indigoai-us/hq-cloud";
32
32
  import { PLACEHOLDER_SUFFIX, companyOffloadKey, containedPath, durablePlaceholderFs, renderPointer, resolvePlaceholder, } from "./storage-offload.js";
33
33
  import { formatPlanGateError, formatUpgradeRefusal, planGateErrorFromResponse, } from "../utils/plan-gate-error.js";
34
34
  /** Metadata key hq-pro binds to `checksumSha256` (sync's content hash). */
@@ -321,7 +321,74 @@ export async function runLibraryAdd(inputs, options, deps) {
321
321
  .catch((err) => ({ status: "failed", note: err.message }));
322
322
  }
323
323
  }
324
- return { company: slug, items };
324
+ const written = items.flatMap((item) => (item.ok && item.placeholderPath ? [item.placeholderPath] : []));
325
+ if (written.length === 0)
326
+ return { company: slug, items };
327
+ const syncScope = await ensureLibraryInSyncScope(company, dirRel, written, deps);
328
+ return { company: slug, items, ...(syncScope ? { syncScope } : {}) };
329
+ }
330
+ const LIBRARY_FOLDER = "library/";
331
+ /**
332
+ * A `shared` or `custom` sync pushes only paths inside the membership's
333
+ * prefix set (explicit grants plus pins), so a placeholder written outside it
334
+ * never reaches the vault. Pin it with `hq files get`'s pin writer: the
335
+ * default library folder as a whole, and for any other `--to` folder only the
336
+ * written placeholders, so the rest of that folder stays out of sync.
337
+ */
338
+ export async function ensureLibraryInSyncScope(company, dirRel, placeholderPaths, deps) {
339
+ if (!deps.syncScope || !deps.addPin)
340
+ return undefined;
341
+ const companyRel = (rel) => rel.split("/").slice(2).join("/");
342
+ const isLibrary = `${companyRel(dirRel)}/` === LIBRARY_FOLDER;
343
+ const targets = isLibrary ? [LIBRARY_FOLDER] : placeholderPaths.map(companyRel);
344
+ const command = isLibrary ? `hq files get ${LIBRARY_FOLDER} --company ${company.slug}` : undefined;
345
+ let scope;
346
+ try {
347
+ scope = await deps.syncScope(company);
348
+ }
349
+ catch (err) {
350
+ return {
351
+ status: "not-pinned",
352
+ pins: targets,
353
+ ...(command ? { command } : {}),
354
+ reason: `could not read your sync scope: ${err.message}`,
355
+ };
356
+ }
357
+ const missing = scope.syncMode === "all" ? [] : targets.filter((t) => !isCoveredByAny(t, scope.prefixSet ?? []));
358
+ if (missing.length === 0)
359
+ return { status: "in-scope", pins: [] };
360
+ try {
361
+ for (const target of missing)
362
+ deps.addPin(deps.hqRoot, company.slug, target);
363
+ }
364
+ catch (err) {
365
+ return {
366
+ status: "not-pinned",
367
+ pins: missing,
368
+ ...(command ? { command } : {}),
369
+ reason: `could not write the pin: ${err.message}`,
370
+ };
371
+ }
372
+ return { status: "pinned", pins: missing };
373
+ }
374
+ /**
375
+ * {@link resolvePullScope} answers `all` when any call fails. Report the
376
+ * failure instead, so a failed lookup is not mistaken for full sync.
377
+ */
378
+ export async function resolveLibrarySyncScope(client, company, hqRoot) {
379
+ let failure;
380
+ const watch = (call) => call.catch((err) => {
381
+ failure ??= err;
382
+ throw err;
383
+ });
384
+ const scope = await resolvePullScope({
385
+ listMyMemberships: () => watch(client.listMyMemberships()),
386
+ getMembershipSyncConfig: (id) => watch(client.getMembershipSyncConfig(id)),
387
+ listMyExplicitGrants: (uid) => watch(client.listMyExplicitGrants(uid)),
388
+ }, company.uid, company.slug, hqRoot);
389
+ if (failure !== undefined)
390
+ throw failure instanceof Error ? failure : new Error(String(failure));
391
+ return scope;
325
392
  }
326
393
  async function writeAssetPlaceholder(p, options, deps, dirAbs) {
327
394
  const ts = offloadTimestamp(deps.now());
@@ -35,7 +35,7 @@ import { findHqRoot } from "../utils/manifest.js";
35
35
  import { networkTransportErrorCode } from "../utils/network-transport-error.js";
36
36
  import { resolveVaultCredentialForCapability } from "../utils/resolve-vault-credential.js";
37
37
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
38
- import { defaultLibraryIo, runLibraryAdd, runLibraryTag, } from "./library-add.js";
38
+ import { defaultLibraryIo, resolveLibrarySyncScope, runLibraryAdd, runLibraryTag, } from "./library-add.js";
39
39
  import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
40
40
  import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
41
41
  /** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
@@ -295,8 +295,39 @@ export function formatLibraryAddResult(result) {
295
295
  return `${line}\n ${chalk.dim(`AI caption added (${item.caption.captionedBy}).`)}`;
296
296
  return `${line}\n ${chalk.yellow(item.caption.note ?? "Captioning skipped.")}`;
297
297
  })
298
+ .concat(formatLibrarySyncScope(result))
298
299
  .join("\n");
299
300
  }
301
+ function formatLibrarySyncScope({ company, syncScope }) {
302
+ if (!syncScope || syncScope.status === "in-scope")
303
+ return [];
304
+ const pins = syncScope.pins.map((pin) => `${company}:${pin}`).join(", ");
305
+ if (syncScope.status === "pinned") {
306
+ return [chalk.dim(`Pinned ${pins} so sync uploads these placeholders to the company vault.`)];
307
+ }
308
+ const fix = syncScope.command
309
+ ? `Pin the folder with: ${syncScope.command}`
310
+ : `Add ${syncScope.pins.join(", ")} under "${company}" in .hq/pins.json at the HQ root.`;
311
+ return [
312
+ chalk.yellow(`Warning: sync may not upload these placeholders (${syncScope.reason}). ` +
313
+ `Until it does, teammates and cloud search will not see them. ${fix}`),
314
+ ];
315
+ }
316
+ /** Sync scope wiring for `hq library add`; an API key has no sync tree to check. */
317
+ async function librarySyncScopeDeps(credential, hqRoot) {
318
+ if (credential.kind !== "cognito")
319
+ return {};
320
+ const [{ VaultClient }, { buildVaultConfig }, { addPin }] = await Promise.all([
321
+ import("@indigoai-us/hq-cloud"),
322
+ import("../utils/cognito-session.js"),
323
+ import("./files-browse.js"),
324
+ ]);
325
+ const client = new VaultClient(buildVaultConfig(credential.token));
326
+ return {
327
+ syncScope: (company) => resolveLibrarySyncScope(client, company, hqRoot),
328
+ addPin,
329
+ };
330
+ }
300
331
  /** GET a presigned URL. Errors never echo the URL, which carries a signature. */
301
332
  async function downloadPresigned(url) {
302
333
  const res = await fetch(url);
@@ -366,9 +397,11 @@ export function registerLibraryCommand(program) {
366
397
  .option("--json", "Print machine-readable JSON")
367
398
  .action(async (sources, options) => {
368
399
  try {
369
- const { token } = await resolveVaultCredentialForCapability("files:write", "library add", {
400
+ const credential = await resolveVaultCredentialForCapability("files:write", "library add", {
370
401
  interactive: true,
371
402
  });
403
+ const { token } = credential;
404
+ const hqRoot = findHqRoot();
372
405
  const result = await runLibraryAdd(sources, {
373
406
  company: options.company,
374
407
  to: options.to,
@@ -376,11 +409,12 @@ export function registerLibraryCommand(program) {
376
409
  title: options.title,
377
410
  note: options.note,
378
411
  }, {
379
- hqRoot: findHqRoot(),
412
+ hqRoot,
380
413
  now: () => new Date(),
381
414
  resolveCompany: (slug) => libraryCompany(token, slug),
382
415
  presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
383
416
  ...defaultLibraryIo,
417
+ ...(await librarySyncScopeDeps(credential, hqRoot)),
384
418
  ...(options.caption === false
385
419
  ? {}
386
420
  : {
@@ -30,6 +30,23 @@ export interface ActiveMember {
30
30
  personName?: string;
31
31
  personSlug?: string;
32
32
  }
33
+ /**
34
+ * The company's active Outpost, as listed beside the members by
35
+ * `GET /membership/company/{companyUid}`. It is read-only: it can be granted
36
+ * secrets (`hq secrets share --with out_…`, `hq groups add`), but it cannot be
37
+ * invited, promoted or removed.
38
+ */
39
+ export interface CompanyOutpostMember {
40
+ uid: string;
41
+ kind: "outpost";
42
+ label: string;
43
+ outpostId: string;
44
+ readOnly: true;
45
+ }
46
+ export interface CompanyRoster {
47
+ members: ActiveMember[];
48
+ outposts: CompanyOutpostMember[];
49
+ }
33
50
  export interface InviteOptions {
34
51
  target: string;
35
52
  role: string;
@@ -165,7 +182,25 @@ export declare function pendingInviteJson(inv: PendingInvite): {
165
182
  invited_at: string;
166
183
  membership_key: string;
167
184
  };
185
+ /** JSON row for a company Outpost in `hq members list --json`. */
186
+ export declare function companyOutpostJson(o: CompanyOutpostMember): {
187
+ person_id: string;
188
+ kind: "outpost";
189
+ label: string;
190
+ outpost_id: string;
191
+ read_only: true;
192
+ };
168
193
  export declare function listActiveMembers(token: string, companyUid: string): Promise<ActiveMember[]>;
194
+ /**
195
+ * Active members plus the company's active Outpost. Servers that predate
196
+ * Outpost listing omit `outposts`; that reads as none.
197
+ */
198
+ export declare function listCompanyRoster(token: string, companyUid: string): Promise<CompanyRoster>;
199
+ /**
200
+ * The company Outpost is listed read-only. hq-pro rejects promote, remove and
201
+ * revoke for it (OUTPOST_MEMBER_READ_ONLY); say so before any request goes out.
202
+ */
203
+ export declare const OUTPOST_READ_ONLY_MESSAGE: string;
169
204
  /**
170
205
  * Resolve a role-change target to an active membership key.
171
206
  *
@@ -4,6 +4,7 @@ import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
4
4
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
5
5
  import { isPlanGateError } from "../utils/plan-gate-error.js";
6
6
  import { queueCliFeatureUsed } from "../utils/cli-telemetry.js";
7
+ import { OUTPOST_UID_PATTERN } from "./_patterns.js";
7
8
  const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
8
9
  const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
9
10
  const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
@@ -261,7 +262,24 @@ export function pendingInviteJson(inv) {
261
262
  membership_key: inv.membershipKey,
262
263
  };
263
264
  }
265
+ /** JSON row for a company Outpost in `hq members list --json`. */
266
+ export function companyOutpostJson(o) {
267
+ return {
268
+ person_id: o.uid,
269
+ kind: "outpost",
270
+ label: o.label || "Outpost",
271
+ outpost_id: o.outpostId,
272
+ read_only: true,
273
+ };
274
+ }
264
275
  export async function listActiveMembers(token, companyUid) {
276
+ return (await listCompanyRoster(token, companyUid)).members;
277
+ }
278
+ /**
279
+ * Active members plus the company's active Outpost. Servers that predate
280
+ * Outpost listing omit `outposts`; that reads as none.
281
+ */
282
+ export async function listCompanyRoster(token, companyUid) {
265
283
  const res = await vaultApiFetch({
266
284
  token,
267
285
  path: `/membership/company/${encodeURIComponent(companyUid)}`,
@@ -270,10 +288,26 @@ export async function listActiveMembers(token, companyUid) {
270
288
  const err = (await res.json().catch(() => ({})));
271
289
  throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
272
290
  }
273
- // Server schema: `{ members: [...] }` — active members only, enriched with
274
- // resolved person metadata (personEmail / personName / personSlug).
291
+ // Server schema: `{ members: [...], outposts: [...] }` — active members
292
+ // only, enriched with resolved person metadata (personEmail / personName /
293
+ // personSlug), plus the read-only company Outpost rows.
275
294
  const data = (await res.json());
276
- return data?.members ?? [];
295
+ const outposts = (data?.outposts ?? []).filter((o) => o?.kind === "outpost" &&
296
+ typeof o.uid === "string" &&
297
+ OUTPOST_UID_PATTERN.test(o.uid));
298
+ return { members: data?.members ?? [], outposts };
299
+ }
300
+ /**
301
+ * The company Outpost is listed read-only. hq-pro rejects promote, remove and
302
+ * revoke for it (OUTPOST_MEMBER_READ_ONLY); say so before any request goes out.
303
+ */
304
+ export const OUTPOST_READ_ONLY_MESSAGE = "An Outpost is shown read-only and cannot be invited, promoted or removed. " +
305
+ "To change what it can read, use `hq secrets share` / `hq secrets unshare` or `hq groups`.";
306
+ function assertNotOutpost(target) {
307
+ const uid = (target.split("#")[0] ?? "").trim();
308
+ if (OUTPOST_UID_PATTERN.test(uid)) {
309
+ throw new Error(OUTPOST_READ_ONLY_MESSAGE);
310
+ }
277
311
  }
278
312
  /**
279
313
  * Resolve a role-change target to an active membership key.
@@ -283,6 +317,7 @@ export async function listActiveMembers(token, companyUid) {
283
317
  * active member list instead of synthesizing an `email:...#company` key.
284
318
  */
285
319
  export async function resolveRoleChangeTarget(token, companyUid, target) {
320
+ assertNotOutpost(target);
286
321
  if (target.includes("#"))
287
322
  return target;
288
323
  const detected = detectTarget(target);
@@ -325,6 +360,7 @@ export async function resolveRoleChangeTarget(token, companyUid, target) {
325
360
  * that exact email was just shown by `hq members list`.
326
361
  */
327
362
  export function resolveRevokeTargetToMembershipKey(arg, companyUid) {
363
+ assertNotOutpost(arg);
328
364
  if (arg.includes("#"))
329
365
  return arg;
330
366
  const detected = detectTarget(arg);
@@ -820,33 +856,48 @@ export function registerMembersCommand(program) {
820
856
  }
821
857
  // Default: list ACTIVE members so their emails drop straight into
822
858
  // `hq secrets share <path> --with <email>`.
823
- const activeMembers = await listActiveMembers(token, companyUid);
859
+ // The company Outpost is listed after the people, read-only; its
860
+ // out_ id drops into `hq secrets share <path> --with out_…`.
861
+ const { members: activeMembers, outposts } = await listCompanyRoster(token, companyUid);
824
862
  if (opts.json) {
825
863
  console.log(JSON.stringify({
826
864
  company_uid: companyUid,
827
865
  members: activeMembers.map(activeMemberJson),
866
+ outposts: outposts.map(companyOutpostJson),
828
867
  }, null, 2));
829
868
  return;
830
869
  }
831
- if (activeMembers.length === 0) {
870
+ if (activeMembers.length === 0 && outposts.length === 0) {
832
871
  console.log(chalk.gray("No active members found for this company."));
833
872
  return;
834
873
  }
835
- const emailW = Math.max(5, ...activeMembers.map((m) => (m.personEmail ?? m.personUid).length));
836
- const roleW = Math.max(4, ...activeMembers.map((m) => m.role.length));
837
- const idW = Math.max(9, ...activeMembers.map((m) => m.personUid.length));
874
+ const rows = [
875
+ ...activeMembers.map((m) => ({
876
+ email: m.personEmail ?? m.personUid,
877
+ role: m.role,
878
+ id: m.personUid,
879
+ name: m.personName ?? m.personSlug ?? "",
880
+ })),
881
+ ...outposts.map((o) => ({
882
+ email: o.uid,
883
+ role: "outpost",
884
+ id: o.uid,
885
+ name: `${o.label || "Outpost"} (read-only)`,
886
+ })),
887
+ ];
888
+ const emailW = Math.max(5, ...rows.map((r) => r.email.length));
889
+ const roleW = Math.max(4, ...rows.map((r) => r.role.length));
890
+ const idW = Math.max(9, ...rows.map((r) => r.id.length));
838
891
  const header = ["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW)];
839
892
  if (opts.ids)
840
893
  header.push("PERSON_ID".padEnd(idW));
841
894
  header.push("NAME");
842
895
  console.log(chalk.bold(header.join(" ")));
843
- for (const m of activeMembers) {
844
- const email = m.personEmail ?? m.personUid;
845
- const name = m.personName ?? m.personSlug ?? "";
846
- const row = [email.padEnd(emailW), m.role.padEnd(roleW)];
896
+ for (const r of rows) {
897
+ const row = [r.email.padEnd(emailW), r.role.padEnd(roleW)];
847
898
  if (opts.ids)
848
- row.push(m.personUid.padEnd(idW));
849
- row.push(name);
899
+ row.push(r.id.padEnd(idW));
900
+ row.push(r.name);
850
901
  console.log(row.join(" "));
851
902
  }
852
903
  }
@@ -1,3 +1,5 @@
1
+ import { reconcileHookRegistrations } from '../lib/hook-engine/reconcile.js';
2
+ import { trustCodexUserHooks } from '../utils/hook-trust.js';
1
3
  /**
2
4
  * hq reindex — surface namespaced skills and commands, clean up retired
3
5
  * personal-overlay mirror links, capture coding-harness session logs, regenerate
@@ -775,6 +777,23 @@ export function registerReindexCommand(program) {
775
777
  // means a sync/rescue holds the shared operation lock and may be
776
778
  // rewriting .claude/settings.json right now, and this is an unlocked
777
779
  // whole-file read-modify-write.
780
+ // Registration reconcile is best-effort: a failure leaves the current
781
+ // registrations in place and must not stop the rest of reindex.
782
+ try {
783
+ // A file that failed is left as it was; the keys of files that were
784
+ // rewritten are still trusted so Codex keeps running their hooks.
785
+ const { codexUserKeys, failures } = reconcileHookRegistrations(hqRoot);
786
+ for (const failure of failures)
787
+ console.warn(`reindex: hook registration reconcile skipped ${failure}`);
788
+ if (codexUserKeys.length) {
789
+ const trust = await trustCodexUserHooks(hqRoot, codexUserKeys);
790
+ if (trust.status === 'failed')
791
+ console.warn(`reindex: Codex hook trust needs repair: ${trust.reason}`);
792
+ }
793
+ }
794
+ catch (error) {
795
+ console.warn(`reindex: hook registration reconcile skipped: ${error.message}`);
796
+ }
778
797
  dropProjectDefaultModeForBypassUser(hqRoot);
779
798
  await trustHqRuntimeHooks(hqRoot, undefined, {
780
799
  rootAliases: [literalHqRoot],