@indigoai-us/hq-cli 5.109.5 → 5.109.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.109.7] — 2026-09-10
6
+
7
+ ### Fixed
8
+
9
+ - Add `whoami --json`, structured `files acl --json` with explicit absence,
10
+ and `people resolve --membership-only` with canonical company/person IDs.
11
+ Delegation helpers can now verify exact grants without parsing display tables
12
+ or treating a missing ACL as a transport failure. Existing human output stays available.
13
+
14
+ ## [5.109.6] — 2026-09-10
15
+
16
+ ### Fixed
17
+
18
+ - A torn hq install no longer files an unactionable crash when a bundled
19
+ third-party dependency of a dependency is missing (HQ-CLI-1Q). `hq packs list`
20
+ loaded the mesh presence client, whose `import mqtt` chain reached
21
+ mqtt-packet's `parser.js` requiring `bl` — mqtt-packet's OWN declared
22
+ transitive dependency — which a partial Windows global install had left
23
+ unwritten. The in-process incomplete-install classifier shipped in 5.108.14
24
+ recognised only a RELATIVE missing specifier for the CJS shape, so this BARE
25
+ specifier fell through to a bare Sentry crash plus an `hq:` line the operator
26
+ could not act on. The classifier now also recognises a bare miss from UNDER
27
+ the running install's own `node_modules/`, but ONLY when the missing specifier
28
+ names a WHOLE package (not a subpath like `bl/lib/x`, which can mean the
29
+ package is present and only that file gone to a version mismatch) AND the
30
+ REQUIRING third-party package's own manifest declares it in its REQUIRED
31
+ `dependencies` (`optionalDependencies` are excluded, being not guaranteed
32
+ installed) — a correct install always writes such a
33
+ dependency, so its absence beside a present requirer can only be a torn
34
+ install, never an hq-cli manifest defect. Those are printed with the same
35
+ input-free reinstall remedy and skipped, at the top-level boundary and in the
36
+ shared `beforeSend`. A bare miss the requirer does NOT declare (a possible
37
+ undeclared or peer-only dependency) stays reported, now enriched on every
38
+ capture route with a bounded `incomplete_install` context naming both packages
39
+ so the next occurrence is attributable instead of bare.
40
+
5
41
  ## [5.109.5] — 2026-09-10
6
42
 
7
43
  ## [5.109.4] — 2026-09-09
@@ -344,12 +380,13 @@
344
380
  `npm i -g @indigoai-us/hq-cli` / `pnpm add -g @indigoai-us/hq-cli`) while
345
381
  skipping Sentry capture, the same disposition established for the qmd child in
346
382
  HQ-CLI-Y. An hq-cli packaging fault stays reportable: a miss under the
347
- package's own `dist/` or `assets/`, a bare-specifier miss (a possible
348
- undeclared dependency), and an esm-loader ENOENT whose path did not survive
349
- delivery are NOT suppressed the last is captured WITH a bounded
350
- `incomplete_install` context so the next occurrence is attributable. The drop
351
- is wired both at the top-level boundary and in the shared `beforeSend`, so it
352
- covers every capture route.
383
+ package's own `dist/` or `assets/`, a bare-specifier miss (then kept loud as a
384
+ possible undeclared dependency refined for a dependency the requiring package
385
+ actually declares in HQ-CLI-1Q, see Unreleased), and an esm-loader ENOENT whose
386
+ path did not survive delivery are NOT suppressed — the last is captured WITH a
387
+ bounded `incomplete_install` context so the next occurrence is attributable.
388
+ The drop is wired both at the top-level boundary and in the shared
389
+ `beforeSend`, so it covers every capture route.
353
390
 
354
391
  ## [5.108.13] — 2026-09-06
355
392
 
@@ -276,7 +276,8 @@ export function registerFilesCommand(program) {
276
276
  files
277
277
  .command("acl <prefix>")
278
278
  .description("Show the ACL (access control list) for a file prefix")
279
- .action(async (prefix) => {
279
+ .option("--json", "Output structured ACL data, including explicit absence")
280
+ .action(async (prefix, opts) => {
280
281
  try {
281
282
  // Read-only, so stripping is safe — but a mismatched anchor would show
282
283
  // a DIFFERENT company's ACL than the one the operator pasted.
@@ -321,7 +322,16 @@ export function registerFilesCommand(program) {
321
322
  process.exit(1);
322
323
  }
323
324
  const tree = (await treeRes.json());
325
+ if (!tree || typeof tree.prefix !== 'string' || !Array.isArray(tree.direct)
326
+ || !Array.isArray(tree.inherited) || !Array.isArray(tree.children)
327
+ || !(tree.directRow === null || (typeof tree.directRow === 'object' && tree.directRow))) {
328
+ throw new Error('Invalid ACL tree response');
329
+ }
324
330
  const row = tree.directRow;
331
+ if (opts.json) {
332
+ console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null }, null, 2));
333
+ return;
334
+ }
325
335
  // No own row AND nothing inherited or granted below — preserve the
326
336
  // original "no ACL record" exit path.
327
337
  if (!row && tree.inherited.length === 0 && tree.children.length === 0) {
@@ -20,6 +20,7 @@ interface PeopleCommandDeps {
20
20
  }
21
21
  interface PeopleLookupOpts {
22
22
  localOnly?: boolean;
23
+ membershipOnly?: boolean;
23
24
  json?: boolean;
24
25
  }
25
26
  export declare function activeMemberToPersonRecord(m: ActiveMember): PersonRecord | null;
@@ -41,6 +41,8 @@ export function activeMemberToPersonRecord(m) {
41
41
  slug,
42
42
  name,
43
43
  email,
44
+ personUid: m.personUid,
45
+ companyUid: m.companyUid,
44
46
  role: m.role || undefined,
45
47
  type: "internal",
46
48
  source: `hq-pro membership: /membership/company/${m.companyUid}`,
@@ -147,6 +149,13 @@ async function tryFetchRoster(fetchRoster, hqRoot, slug) {
147
149
  }
148
150
  }
149
151
  export async function resolvePersonWithRosterFallback(input) {
152
+ if (input.opts?.membershipOnly) {
153
+ if (input.opts.localOnly)
154
+ throw new Error('--membership-only cannot be combined with --local-only');
155
+ // No local fallback: callers need authoritative IDs and must see transport errors.
156
+ const roster = await (input.fetchRoster ?? fetchMembershipRoster)(input.hqRoot, input.slug);
157
+ return resolveNameToEmail(roster, input.name);
158
+ }
150
159
  const localPeople = listCompanyPeople(input.hqRoot, input.slug);
151
160
  const local = resolveNameToEmail(localPeople, input.name);
152
161
  if (local.status !== "not_found" || input.opts?.localOnly)
@@ -239,6 +248,7 @@ export function registerPeopleCommand(program, deps = {}) {
239
248
  people
240
249
  .command("resolve <name>")
241
250
  .description("Resolve a person name to their email address")
251
+ .option("--membership-only", "Resolve from authoritative company membership, ignoring local records")
242
252
  .option("--json", "Output JSON instead of plain text")
243
253
  .option("--local-only", "Skip network fallback; resolve only from the local people roster")
244
254
  .action(async (name, opts) => {
@@ -1,17 +1,3 @@
1
- /**
2
- * hq packages install <slug> — download, verify, and extract a package (US-005)
3
- *
4
- * Flow:
5
- * 1. Check auth (prompt login if needed)
6
- * 2. Check entitlement via API
7
- * 3. Download tarball via presigned URL
8
- * 4. Verify SHA256 hash
9
- * 5. Verify RSA signature (if provided)
10
- * 6. Extract to packages/installed/<slug>/
11
- * 7. Validate extracted package.yaml slug matches
12
- * 8. Update packages/registry.yaml
13
- * 9. Print next-step message
14
- */
15
1
  import { Command } from 'commander';
16
2
  export declare function registerPackageInstallCommand(parent: Command): void;
17
3
  //# sourceMappingURL=pkg-install.d.ts.map
@@ -1,28 +1,4 @@
1
- /**
2
- * hq packages install <slug> — download, verify, and extract a package (US-005)
3
- *
4
- * Flow:
5
- * 1. Check auth (prompt login if needed)
6
- * 2. Check entitlement via API
7
- * 3. Download tarball via presigned URL
8
- * 4. Verify SHA256 hash
9
- * 5. Verify RSA signature (if provided)
10
- * 6. Extract to packages/installed/<slug>/
11
- * 7. Validate extracted package.yaml slug matches
12
- * 8. Update packages/registry.yaml
13
- * 9. Print next-step message
14
- */
15
- import * as fs from 'fs';
16
- import * as os from 'os';
17
- import * as path from 'path';
18
- import { parseUserYaml } from '../utils/user-yaml-error.js';
19
- import { execSync } from 'child_process';
20
1
  import chalk from 'chalk';
21
- import { ensureCognitoToken } from '../utils/cognito-session.js';
22
- import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
23
- import { getRegistryUrl, RegistryClient, } from '../utils/registry-client.js';
24
- import { verifySha256, verifyRsaSignature } from '../utils/integrity.js';
25
- import { addToRegistry } from '../utils/registry.js';
26
2
  import { peekHqApiKey, requireApiKeyCapability, } from '../utils/resolve-vault-credential.js';
27
3
  import { MARKETPLACE_PREFIX, installPack, sourceMatchesPackPattern, } from './pack-install.js';
28
4
  /**
@@ -105,107 +81,4 @@ export function registerPackageInstallCommand(parent) {
105
81
  }
106
82
  });
107
83
  }
108
- async function installPackage(slug, company) {
109
- // 1. Auth
110
- const accessToken = await ensureCognitoToken();
111
- const registryUrl = getRegistryUrl();
112
- const client = new RegistryClient(registryUrl, accessToken);
113
- // 2. Check entitlement
114
- console.log(chalk.dim(`Checking entitlement for ${slug}...`));
115
- const entitlement = await client.checkEntitlement(slug);
116
- if (!entitlement.entitled) {
117
- throw new Error(`You are not entitled to package "${slug}". Visit the registry to purchase or request access.`);
118
- }
119
- // 3. Get download URL
120
- console.log(chalk.dim('Fetching download URL...'));
121
- const download = await client.getDownloadUrl(slug);
122
- // 4. Download to temp file
123
- const tmpDir = os.tmpdir();
124
- const tmpFile = path.resolve(tmpDir, `hq-pkg-${slug}-${Date.now()}.tar.gz`);
125
- try {
126
- console.log(chalk.dim('Downloading package...'));
127
- const response = await fetch(download.url, {
128
- signal: AbortSignal.timeout(120_000),
129
- });
130
- if (!response.ok) {
131
- throw new Error(`Download failed (${response.status})`);
132
- }
133
- const buffer = Buffer.from(await response.arrayBuffer());
134
- fs.writeFileSync(tmpFile, buffer);
135
- // 5. Verify SHA256
136
- console.log(chalk.dim('Verifying integrity...'));
137
- const hashValid = await verifySha256(tmpFile, download.sha256);
138
- if (!hashValid) {
139
- throw new Error('SHA256 hash mismatch — the downloaded file may be corrupted or tampered with.');
140
- }
141
- // 6. Verify RSA signature (if provided)
142
- if (download.signature) {
143
- const sigValid = verifyRsaSignature(download.sha256, download.signature);
144
- if (!sigValid) {
145
- throw new Error('RSA signature verification failed — the package may have been tampered with or the public key is missing/invalid. Aborting install.');
146
- }
147
- }
148
- // 7. Extract
149
- const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
150
- const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
151
- // Clean existing installation
152
- if (fs.existsSync(installDir)) {
153
- fs.rmSync(installDir, { recursive: true, force: true });
154
- }
155
- fs.mkdirSync(installDir, { recursive: true });
156
- execSync(`tar -xzf "${tmpFile}" -C "${installDir}"`, {
157
- stdio: 'pipe',
158
- });
159
- // 8. Validate package.yaml slug
160
- const packageYamlPath = path.resolve(installDir, 'package.yaml');
161
- if (fs.existsSync(packageYamlPath)) {
162
- const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
163
- const pkgMeta = parseUserYaml(pkgContent, packageYamlPath);
164
- if (pkgMeta?.slug && pkgMeta.slug !== slug) {
165
- // Mismatch — clean up and abort
166
- fs.rmSync(installDir, { recursive: true, force: true });
167
- throw new Error(`Package slug mismatch: expected "${slug}", got "${pkgMeta.slug}"`);
168
- }
169
- }
170
- // 9. Get package info for registry entry
171
- let version = 'unknown';
172
- let name = slug;
173
- try {
174
- const pkgInfo = await client.getPackage(slug);
175
- version = pkgInfo.package.latest_version;
176
- name = pkgInfo.package.name;
177
- }
178
- catch {
179
- // Best-effort — fall back to package.yaml if present
180
- const packageYaml = path.resolve(installDir, 'package.yaml');
181
- if (fs.existsSync(packageYaml)) {
182
- const content = fs.readFileSync(packageYaml, 'utf-8');
183
- const meta = parseUserYaml(content, packageYaml);
184
- if (meta?.version)
185
- version = meta.version;
186
- if (meta?.name)
187
- name = meta.name;
188
- }
189
- }
190
- // 10. Update registry.yaml
191
- const now = new Date().toISOString();
192
- addToRegistry(hqRoot, {
193
- name,
194
- slug,
195
- version,
196
- source: registryUrl,
197
- scope: company,
198
- installed_at: now,
199
- updated_at: now,
200
- });
201
- console.log(chalk.green(`\nInstalled ${slug}@${version} to packages/installed/${slug}/`));
202
- console.log(chalk.cyan('Run /package-install ' + slug + ' in Claude to merge into your HQ.'));
203
- }
204
- finally {
205
- // Clean up temp file — never leave partial downloads
206
- if (fs.existsSync(tmpFile)) {
207
- fs.unlinkSync(tmpFile);
208
- }
209
- }
210
- }
211
84
  //# sourceMappingURL=pkg-install.js.map
@@ -4,7 +4,7 @@
4
4
  import chalk from 'chalk';
5
5
  import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq-cloud';
6
6
  import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
7
- import { resolveCognitoTokenSource } from "../utils/cognito-session.js";
7
+ import { loadMachineCachedTokens, resolveCognitoTokenSource } from "../utils/cognito-session.js";
8
8
  function peekIdToken(idToken) {
9
9
  const decoded = decodeIdToken(idToken);
10
10
  return {
@@ -22,13 +22,39 @@ export function registerWhoamiCommand(program) {
22
22
  program
23
23
  .command('whoami')
24
24
  .description('Show the currently authenticated user')
25
- .action(async () => {
25
+ .option('--json', 'Output identity metadata as JSON (never tokens)')
26
+ .action(async (opts) => {
26
27
  try {
27
28
  // Effective source (same as auth status / ensureCognitoToken): env
28
29
  // opt-in, fresh-box default-creds when no usable person session, or
29
30
  // after a definitive person-refresh rejection marked the session rejected.
30
31
  const machine = resolveCognitoTokenSource() === "machine";
31
32
  const cached = loadCachedTokens();
33
+ if (opts.json) {
34
+ const machineCreds = machine ? loadMachineCreds() : null;
35
+ const identityCache = machine ? loadMachineCachedTokens() : cached;
36
+ const identity = identityCache ? peekIdToken(identityCache.idToken) : {};
37
+ const claims = identityCache ? decodeIdToken(identityCache.idToken) : {};
38
+ // Modern credentials carry the canonical UID. Legacy caches may only
39
+ // supply a UID when their subject is bound to the selected username.
40
+ const boundLegacyAgent = machineCreds && identity.entityType === 'agent'
41
+ && (claims['cognito:username'] === machineCreds.username
42
+ || claims.username === machineCreds.username || claims.sub === machineCreds.username)
43
+ ? identity.entityUid ?? null : null;
44
+ const agentUid = machineCreds?.entityUid !== undefined
45
+ ? (machineCreds.entityUid.startsWith('agt_') ? machineCreds.entityUid : null)
46
+ : boundLegacyAgent;
47
+ console.log(JSON.stringify({
48
+ schemaVersion: 1,
49
+ authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
50
+ tokenSource: machine ? 'machine' : 'person',
51
+ email: machine ? null : identity.email ?? null,
52
+ personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
53
+ agentUid,
54
+ username: machineCreds?.username ?? null,
55
+ }, null, 2));
56
+ return;
57
+ }
32
58
  if (machine) {
33
59
  // Machine identities (company agents) mint sessions on demand from
34
60
  // long-lived creds — report the machine identity honestly even when
@@ -50,7 +50,7 @@ export declare const POLICY_TRIGGER_LEDGER_RELPATH = "workspace/orchestrator/pol
50
50
  /** The two lifecycle events `check-hq-hooks.sh` requires a command hook on. */
51
51
  export declare const REQUIRED_HOOK_EVENTS: readonly ["SessionStart", "PreToolUse"];
52
52
  /**
53
- * Default runtime marker path an agents-v2 (hermes) box writes its runtime mode
53
+ * Default runtime marker path an agents-v2 (hq-fleet) box writes its runtime mode
54
54
  * to. Overridable via `HQ_RUNTIME_MARKER_FILE`, exactly as the shell reads
55
55
  * `${HQ_RUNTIME_MARKER_FILE:-/var/lib/hq-agent/runtime.json}`.
56
56
  */
@@ -129,31 +129,31 @@ export interface AgentsV2AttestationOptions {
129
129
  */
130
130
  export declare function isAgentsV2Runtime(hqRoot: string, env?: NodeJS.ProcessEnv): boolean;
131
131
  /**
132
- * Env var overriding the agents-v2 runtime (hermes) config path (chiefly for
132
+ * Env var overriding the agents-v2 runtime (hq-fleet) config path (chiefly for
133
133
  * tests), mirroring `HQ_RUNTIME_MARKER_FILE` and the shell's
134
- * `HQ_HERMES_CONFIG_FILE`.
134
+ * `HQ_FLEET_CONFIG_FILE`.
135
135
  */
136
- export declare const HQ_HERMES_CONFIG_ENV = "HQ_HERMES_CONFIG_FILE";
136
+ export declare const HQ_FLEET_CONFIG_ENV = "HQ_FLEET_CONFIG_FILE";
137
137
  /**
138
- * The agents-v2 runtime (hermes) config whose `hooks:` block wires the on-box
139
- * adapter into every lifecycle event — the env override, else `~/.hermes/config.yaml`
138
+ * The agents-v2 runtime (hq-fleet) config whose `hooks:` block wires the on-box
139
+ * adapter into every lifecycle event — the env override, else `~/.hq-fleet/config.yaml`
140
140
  * (where `provision/render-config.sh` renders it). Mirrors the shell's
141
- * `HQ_HERMES_CONFIG_FILE="${HQ_HERMES_CONFIG_FILE:-$HOME/.hermes/config.yaml}"`.
141
+ * `HQ_FLEET_CONFIG_FILE="${HQ_FLEET_CONFIG_FILE:-$HOME/.hq-fleet/config.yaml}"`.
142
142
  */
143
- export declare function resolveHermesConfigPath(env?: NodeJS.ProcessEnv): string;
143
+ export declare function resolveHqFleetConfigPath(env?: NodeJS.ProcessEnv): string;
144
144
  /**
145
145
  * Whether the agents-v2 runtime actually wires the on-box hook adapter.
146
146
  *
147
147
  * The wiring lives in the RUNTIME's hook config, not `.claude/settings.json`. On
148
148
  * a real box the v2 runtime's shell-hook dispatcher (`agent/shell_hooks.py`)
149
- * reads a `hooks:` block from `~/.hermes/config.yaml` with one entry per
149
+ * reads a `hooks:` block from `~/.hq-fleet/config.yaml` with one entry per
150
150
  * lifecycle event, each invoking `hq-agents-v2-hook-adapter.sh`; the adapter in
151
151
  * turn READS `.claude/settings.json` (via `hook-adapter-core.sh`) to fan out to
152
152
  * the classic `.claude/hooks` set. So `.claude/settings.json` never NAMES the
153
153
  * adapter — it wires the classic `hook-gate.sh` hooks — and grepping it for the
154
154
  * adapter always fails on a real box (verified on the v2.17 canary
155
155
  * i-0277243ad3aed8109, 2026-09-05: settings.json had 0 adapter refs / 94
156
- * hook-gate.sh refs, while ~/.hermes/config.yaml wired the adapter across 7
156
+ * hook-gate.sh refs, while ~/.hq-fleet/config.yaml wired the adapter across 7
157
157
  * events). Require BOTH the adapter installed under the tree at
158
158
  * {@link AGENTS_V2_ADAPTER_RELPATH} AND the runtime config invoking it. Mirrors
159
159
  * `hq_runtime_config_wires_v2_adapter` in `check-hq-hooks.sh`.
@@ -169,7 +169,7 @@ export declare function runtimeConfigWiresV2Adapter(hqRoot: string, env?: NodeJS
169
169
  export declare function v2LedgerPresent(opts: AgentsV2AttestationOptions): boolean;
170
170
  /**
171
171
  * All three agents-v2 self-attestation conditions. Used only to GRANT PASS to a
172
- * hermes box that host detection leaves platform-unknown; never to withhold it.
172
+ * hq-fleet box that host detection leaves platform-unknown; never to withhold it.
173
173
  * The exact conjunction of `agents_v2_attested` in `check-hq-hooks.sh`.
174
174
  */
175
175
  export declare function agentsV2Attested(opts: AgentsV2AttestationOptions): boolean;
@@ -53,7 +53,7 @@ export const POLICY_TRIGGER_LEDGER_RELPATH = "workspace/orchestrator/policy-trig
53
53
  /** The two lifecycle events `check-hq-hooks.sh` requires a command hook on. */
54
54
  export const REQUIRED_HOOK_EVENTS = ["SessionStart", "PreToolUse"];
55
55
  /**
56
- * Default runtime marker path an agents-v2 (hermes) box writes its runtime mode
56
+ * Default runtime marker path an agents-v2 (hq-fleet) box writes its runtime mode
57
57
  * to. Overridable via `HQ_RUNTIME_MARKER_FILE`, exactly as the shell reads
58
58
  * `${HQ_RUNTIME_MARKER_FILE:-/var/lib/hq-agent/runtime.json}`.
59
59
  */
@@ -161,36 +161,36 @@ function readRuntimeMarkerMode(markerPath) {
161
161
  }
162
162
  }
163
163
  /**
164
- * Env var overriding the agents-v2 runtime (hermes) config path (chiefly for
164
+ * Env var overriding the agents-v2 runtime (hq-fleet) config path (chiefly for
165
165
  * tests), mirroring `HQ_RUNTIME_MARKER_FILE` and the shell's
166
- * `HQ_HERMES_CONFIG_FILE`.
166
+ * `HQ_FLEET_CONFIG_FILE`.
167
167
  */
168
- export const HQ_HERMES_CONFIG_ENV = "HQ_HERMES_CONFIG_FILE";
168
+ export const HQ_FLEET_CONFIG_ENV = "HQ_FLEET_CONFIG_FILE";
169
169
  /**
170
- * The agents-v2 runtime (hermes) config whose `hooks:` block wires the on-box
171
- * adapter into every lifecycle event — the env override, else `~/.hermes/config.yaml`
170
+ * The agents-v2 runtime (hq-fleet) config whose `hooks:` block wires the on-box
171
+ * adapter into every lifecycle event — the env override, else `~/.hq-fleet/config.yaml`
172
172
  * (where `provision/render-config.sh` renders it). Mirrors the shell's
173
- * `HQ_HERMES_CONFIG_FILE="${HQ_HERMES_CONFIG_FILE:-$HOME/.hermes/config.yaml}"`.
173
+ * `HQ_FLEET_CONFIG_FILE="${HQ_FLEET_CONFIG_FILE:-$HOME/.hq-fleet/config.yaml}"`.
174
174
  */
175
- export function resolveHermesConfigPath(env = process.env) {
176
- const override = env[HQ_HERMES_CONFIG_ENV]?.trim();
175
+ export function resolveHqFleetConfigPath(env = process.env) {
176
+ const override = env[HQ_FLEET_CONFIG_ENV]?.trim();
177
177
  if (override)
178
178
  return override;
179
- return path.join(os.homedir(), ".hermes", "config.yaml");
179
+ return path.join(os.homedir(), ".hq-fleet", "config.yaml");
180
180
  }
181
181
  /**
182
182
  * Whether the agents-v2 runtime actually wires the on-box hook adapter.
183
183
  *
184
184
  * The wiring lives in the RUNTIME's hook config, not `.claude/settings.json`. On
185
185
  * a real box the v2 runtime's shell-hook dispatcher (`agent/shell_hooks.py`)
186
- * reads a `hooks:` block from `~/.hermes/config.yaml` with one entry per
186
+ * reads a `hooks:` block from `~/.hq-fleet/config.yaml` with one entry per
187
187
  * lifecycle event, each invoking `hq-agents-v2-hook-adapter.sh`; the adapter in
188
188
  * turn READS `.claude/settings.json` (via `hook-adapter-core.sh`) to fan out to
189
189
  * the classic `.claude/hooks` set. So `.claude/settings.json` never NAMES the
190
190
  * adapter — it wires the classic `hook-gate.sh` hooks — and grepping it for the
191
191
  * adapter always fails on a real box (verified on the v2.17 canary
192
192
  * i-0277243ad3aed8109, 2026-09-05: settings.json had 0 adapter refs / 94
193
- * hook-gate.sh refs, while ~/.hermes/config.yaml wired the adapter across 7
193
+ * hook-gate.sh refs, while ~/.hq-fleet/config.yaml wired the adapter across 7
194
194
  * events). Require BOTH the adapter installed under the tree at
195
195
  * {@link AGENTS_V2_ADAPTER_RELPATH} AND the runtime config invoking it. Mirrors
196
196
  * `hq_runtime_config_wires_v2_adapter` in `check-hq-hooks.sh`.
@@ -201,7 +201,7 @@ export function runtimeConfigWiresV2Adapter(hqRoot, env = process.env) {
201
201
  return false;
202
202
  let raw;
203
203
  try {
204
- raw = fs.readFileSync(resolveHermesConfigPath(env), "utf8");
204
+ raw = fs.readFileSync(resolveHqFleetConfigPath(env), "utf8");
205
205
  }
206
206
  catch {
207
207
  return false;
@@ -265,7 +265,7 @@ function ledgerDirHasFreshTxt(dir, cutoffMs) {
265
265
  }
266
266
  /**
267
267
  * All three agents-v2 self-attestation conditions. Used only to GRANT PASS to a
268
- * hermes box that host detection leaves platform-unknown; never to withhold it.
268
+ * hq-fleet box that host detection leaves platform-unknown; never to withhold it.
269
269
  * The exact conjunction of `agents_v2_attested` in `check-hq-hooks.sh`.
270
270
  */
271
271
  export function agentsV2Attested(opts) {
@@ -286,7 +286,7 @@ export function checkRuntimeProbe(context) {
286
286
  const checkId = `${RUNTIME_PROBE_PREFIX}.enforcement`;
287
287
  const target = POLICY_TRIGGER_LEDGER_RELPATH;
288
288
  const scope = sessionId ? ` for session ${sessionId}` : "";
289
- // agents-v2 (hermes) self-attestation. hq doctor leaves the hermes host
289
+ // agents-v2 (hq-fleet) self-attestation. hq doctor leaves the hq-fleet host
290
290
  // platform-unknown and, on an unknown host, the probe would report UNKNOWN
291
291
  // below. But the on-box adapter provably wrote the ledger through the same
292
292
  // .claude hooks, so grant PASS — and report platform "agents-v2" — when, and
@@ -75,12 +75,12 @@ export interface DeriveVerdictOptions {
75
75
  */
76
76
  requireLedger?: boolean;
77
77
  /**
78
- * Whether this is an attested agents-v2 (hermes) box: the runtime is
78
+ * Whether this is an attested agents-v2 (hq-fleet) box: the runtime is
79
79
  * agents-v2, `.claude/settings.json` wires the on-box adapter, and a
80
80
  * policy-trigger ledger exists (the exact session's when a session id is
81
81
  * given; otherwise a ledger fresh within the freshness window). When true,
82
82
  * OBSERVED is granted even though the doctor's runtime check did not report
83
- * PASS — because hq doctor leaves the hermes host platform-unknown, yet the
83
+ * PASS — because hq doctor leaves the hq-fleet host platform-unknown, yet the
84
84
  * on-box adapter provably wrote the ledger through the same .claude hooks.
85
85
  *
86
86
  * This is the exact twin of the `agents_v2_attested` override in
@@ -81,7 +81,7 @@ export function deriveCheckHqHooksVerdict(doc, options = {}) {
81
81
  // ledger" contract.
82
82
  const runtimeResult = doc.results.find((result) => result.checkId === RUNTIME_ENFORCEMENT_CHECK_ID);
83
83
  let runtime = observeRuntime(runtimeResult?.status, requireLedger);
84
- // agents-v2 self-attestation: hq doctor leaves the hermes host
84
+ // agents-v2 self-attestation: hq doctor leaves the hq-fleet host
85
85
  // platform-unknown and so does not report the runtime check as PASS, but the
86
86
  // on-box adapter provably wrote the ledger through the same .claude hooks.
87
87
  // Grant the identical OBSERVED verdict rather than relaying the host-unknown
package/dist/main.js CHANGED
@@ -563,21 +563,24 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
563
563
  // An IN-PROCESS module-load failure that means hq-cli's OWN installed
564
564
  // package tree is incomplete at load time — a partial/interrupted global
565
565
  // install left a bundled file unwritten (HQ-CLI-1N, a CJS relative-sibling
566
- // MODULE_NOT_FOUND), or a concurrent global install rewrote the running
567
- // tree so an ESM module present at resolve was gone at read (HQ-CLI-1M, an
568
- // esm-loader ENOENT). Both carry no hq-cli frames and reached the final
569
- // else, filing a bare crash and an unactionable line. An incomplete
570
- // install is the caller's machine, the disposition HQ-CLI-Y already
571
- // established for the qmd CHILD print the input-free reinstall remedy and
572
- // skip capture. Placed with the environmental family (after the typed qmd
573
- // carriers and the hq state-write carrier, before environmentalFsErrorMessage):
574
- // the signatures are disjoint ENVIRONMENTAL_FS_CODES is only
575
- // ENOSPC/EDQUOT/EROFS (never ENOENT/MODULE_NOT_FOUND), no qmd carrier sets
576
- // requireStack or an esm-loader frame, and the classified file must sit
577
- // under <packageRoot>/node_modules so ordering changes nothing that
578
- // exists. The UNATTRIBUTABLE shape (an esm-loader ENOENT whose path did not
579
- // survive) is deliberately NOT suppressed; it is captured WITH bounded
580
- // context on the generic path below.
566
+ // MODULE_NOT_FOUND; HQ-CLI-1Q, a CJS BARE-specifier MODULE_NOT_FOUND for a
567
+ // dependency the requiring third-party package's own manifest declares), or
568
+ // a concurrent global install rewrote the running tree so an ESM module
569
+ // present at resolve was gone at read (HQ-CLI-1M, an esm-loader ENOENT).
570
+ // All carry no hq-cli frames and reached the final else, filing a bare
571
+ // crash and an unactionable line. An incomplete install is the caller's
572
+ // machine, the disposition HQ-CLI-Y already established for the qmd CHILD —
573
+ // print the input-free reinstall remedy and skip capture. Placed with the
574
+ // environmental family (after the typed qmd carriers and the hq state-write
575
+ // carrier, before environmentalFsErrorMessage): the signatures are disjoint
576
+ // ENVIRONMENTAL_FS_CODES is only ENOSPC/EDQUOT/EROFS (never
577
+ // ENOENT/MODULE_NOT_FOUND), no qmd carrier sets requireStack or an
578
+ // esm-loader frame, and the classified file must sit under
579
+ // <packageRoot>/node_modules so ordering changes nothing that exists. The
580
+ // UNATTRIBUTABLE shapes are deliberately NOT suppressed and are captured
581
+ // WITH bounded context on the generic path below: an esm-loader ENOENT
582
+ // whose path did not survive, and a bare miss the requirer does NOT declare
583
+ // (a possible undeclared/peer dependency, now named in the context).
581
584
  const incompleteInstallMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || workdirMissingMsg || stateWriteMsg
582
585
  ? null
583
586
  : incompleteInstallMessage(err);
package/dist/sentry.js CHANGED
@@ -6,7 +6,7 @@ import { CLI_VERSION } from "./cli-version.js";
6
6
  import { getCachedSentryUser } from "./utils/sentry-identity.js";
7
7
  import { isEpipe } from "./utils/epipe.js";
8
8
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
9
- import { incompleteInstallMessage } from "./utils/incomplete-install-error.js";
9
+ import { incompleteInstallCaptureContext, incompleteInstallMessage, } from "./utils/incomplete-install-error.js";
10
10
  import { sentryFingerprintFor } from "./utils/sentry-fingerprint.js";
11
11
  /**
12
12
  * Drop broken-pipe (EPIPE) crashes before scrubbing/send. A closed downstream
@@ -35,17 +35,33 @@ export function epipeAwareBeforeSend(event, hint) {
35
35
  // Path-independent belt for an IN-PROCESS incomplete-install module-load
36
36
  // failure — hq-cli's own globally installed tree is not intact at load time
37
37
  // (HQ-CLI-1N, a CJS relative-sibling MODULE_NOT_FOUND under its bundled
38
- // node_modules; HQ-CLI-1M, an esm-loader ENOENT for a file present at resolve
39
- // and gone at read). handleTopLevelError already prints the reinstall remedy
40
- // for the top-level route; dropping the event here suppresses the fatal
41
- // regardless of route the unhandled-rejection boundary, the command-level
42
- // captureException sites, and bin/hq-auth-refreshmirroring the EPIPE and
43
- // environmental-fs drops above. The classifier reads only structured fields
44
- // and the failing file must sit under the running install's node_modules, so
45
- // an hq-cli packaging fault (a dist/ miss, a bare-specifier miss) and the
38
+ // node_modules; HQ-CLI-1Q, a CJS BARE-specifier MODULE_NOT_FOUND the requiring
39
+ // third-party package's own manifest declares; HQ-CLI-1M, an esm-loader ENOENT
40
+ // for a file present at resolve and gone at read). handleTopLevelError already
41
+ // prints the reinstall remedy for the top-level route; dropping the event here
42
+ // suppresses the fatal regardless of route — the unhandled-rejection boundary,
43
+ // the command-level captureException sites, and bin/hq-auth-refresh
44
+ // mirroring the EPIPE and environmental-fs drops above. The classifier reads
45
+ // only structured fields (plus, for the bare shape, the requiring package's
46
+ // own manifest) and the failing file must sit under the running install's
47
+ // node_modules, so an hq-cli packaging fault (a dist/ miss, or a bare miss the
48
+ // requirer does NOT declare — a possible undeclared/peer dependency) and the
46
49
  // path-less unattributable shape are NOT dropped here and stay captured.
47
50
  if (incompleteInstallMessage(hint?.originalException))
48
51
  return null;
52
+ // An incomplete-install shape that is NOT suppressed (a bare miss the requirer
53
+ // does not declare, or a path-less esm-loader ENOENT) should still arrive
54
+ // ATTRIBUTABLE on every capture route, not only the top-level boundary:
55
+ // handleTopLevelError in main.ts is the ONLY caller of
56
+ // incompleteInstallCaptureContext, so events reaching this hook directly — the
57
+ // unhandled-rejection integration, the command-level captureException sites,
58
+ // and bin/hq-auth-refresh — would otherwise survive bare. Merge the bounded
59
+ // context here too. Idempotent: main.ts attaches the same block on its route,
60
+ // and an already-present context wins the spread.
61
+ const installContext = incompleteInstallCaptureContext(hint?.originalException);
62
+ if (installContext) {
63
+ event.contexts = { ...installContext, ...event.contexts };
64
+ }
49
65
  // Group an event that survives to send by a BOUNDED machine discriminator so
50
66
  // unrelated gateway/HTTP failures stop colliding into one fungible issue
51
67
  // (HQ-CLI collision, Sentry 7642756130). Placed here — path-independent,
@@ -21,9 +21,9 @@ export type PackageRootResolver = () => string | null;
21
21
  * means "handle as usual (capture to Sentry)". Never throws — a resolver that
22
22
  * fails yields null.
23
23
  */
24
- export declare function incompleteInstallMessage(err: unknown, resolvePackageRoot?: PackageRootResolver): string | null;
25
- /** Bounded, scrubber-safe diagnostics for an unattributable esm-loader ENOENT. */
26
- export type IncompleteInstallDiagnostics = {
24
+ export declare function incompleteInstallMessage(err: unknown, resolvePackageRoot?: PackageRootResolver, fileSystem?: Pick<typeof fs, "readFileSync">): string | null;
25
+ /** Bounded, scrubber-safe diagnostics for an unattributable esm-loader ENOENT (HQ-CLI-1M). */
26
+ export type IncompleteInstallEsmDiagnostics = {
27
27
  packageRoot: string;
28
28
  packageJsonExists: boolean;
29
29
  nodeModulesExists: boolean;
@@ -31,20 +31,42 @@ export type IncompleteInstallDiagnostics = {
31
31
  code: string;
32
32
  };
33
33
  /**
34
- * When an esm-loader ENOENT reaches the capture path WITHOUT being suppressed —
35
- * the exact shape the delivered HQ-CLI-1M payload arrived in, where neither the
36
- * exception value nor node_system_error carried a `path` return a bounded
37
- * `contexts.incomplete_install` block so the next occurrence carries the
38
- * evidence this one lacked; otherwise return undefined (bare capture). Built
39
- * with the byte-capped, scrubber-safe discipline of package-root-diagnostics.ts:
40
- * the resolved package root and whether its package.json / node_modules exist,
41
- * the loader-frame marker, and the bounded errno code — never a caller argv,
42
- * query, or user-minted value. Never throws.
43
- *
44
- * main.ts attaches this on the generic capture path exactly as
45
- * qmdSpawnFailureCaptureContext already does.
34
+ * Bounded, scrubber-safe diagnostics for a NOT-suppressed third-party bare miss
35
+ * (HQ-CLI-1Q): a possible undeclared / peer-only dependency defect, made
36
+ * attributable the missing package, the requiring package, and the fact that
37
+ * the requirer's own manifest did NOT declare it. Both names live inside
38
+ * hq-cli's dependency graph, so grouping cardinality stays bounded.
39
+ */
40
+ export type IncompleteInstallBareDiagnostics = {
41
+ missingPackage: string;
42
+ requiringPackage: string;
43
+ requirerDeclaresMissing: false;
44
+ };
45
+ /**
46
+ * Both enriched shapes as a single OPEN record — every field optional so a
47
+ * consumer can forward either shape to Sentry without narrowing (the boundary
48
+ * and beforeSend only pass the block through). Every value CONSTRUCTED here is
49
+ * exactly one of the two strict shapes above; the looseness is only at the read
50
+ * boundary.
51
+ */
52
+ export type IncompleteInstallDiagnostics = Partial<IncompleteInstallEsmDiagnostics & IncompleteInstallBareDiagnostics>;
53
+ /**
54
+ * When an incomplete-install failure reaches the capture path WITHOUT being
55
+ * suppressed, return a bounded `contexts.incomplete_install` block so the next
56
+ * occurrence carries the evidence this one lacked; otherwise return undefined
57
+ * (bare capture). Two enriched shapes:
58
+ * - a third-party BARE-specifier miss the requirer did not declare (HQ-CLI-1Q)
59
+ * → { missingPackage, requiringPackage, requirerDeclaresMissing:false };
60
+ * - an esm-loader ENOENT whose path did not survive delivery (HQ-CLI-1M) — the
61
+ * shape the delivered payload arrived in, where neither the exception value
62
+ * nor node_system_error carried a `path` → { packageRoot, packageJsonExists,
63
+ * nodeModulesExists, esmLoaderFrame, code }.
64
+ * Built with the byte-capped, scrubber-safe discipline of
65
+ * package-root-diagnostics.ts — never a caller argv, query, or user-minted
66
+ * value. Never throws. main.ts attaches this on the generic capture path
67
+ * exactly as qmdSpawnFailureCaptureContext already does.
46
68
  */
47
- export declare function incompleteInstallCaptureContext(err: unknown, resolvePackageRoot?: PackageRootResolver, fileSystem?: Pick<typeof fs, "existsSync">): {
69
+ export declare function incompleteInstallCaptureContext(err: unknown, resolvePackageRoot?: PackageRootResolver, fileSystem?: Pick<typeof fs, "existsSync"> & Partial<Pick<typeof fs, "readFileSync">>): {
48
70
  incomplete_install: IncompleteInstallDiagnostics;
49
71
  } | undefined;
50
72
  //# sourceMappingURL=incomplete-install-error.d.ts.map
@@ -7,7 +7,7 @@
7
7
  // failure inside THIS process rather than a qmd child's captured stderr, which
8
8
  // is the gap HQ-CLI-Y's classifier cannot cover.
9
9
  //
10
- // Two shapes, one cause — a partial/interrupted global install left a file
10
+ // Three shapes, one cause — a partial/interrupted global install left a file
11
11
  // unwritten, or a concurrent global install rewrote the running tree
12
12
  // underneath a command:
13
13
  //
@@ -19,6 +19,16 @@
19
19
  // specifier internal to a third-party package can only be a truncated on-disk
20
20
  // copy, never an hq-cli manifest defect.
21
21
  //
22
+ // HQ-CLI-1Q (Sentry 7716586928) — CJS, in-process. The SAME `import mqtt`
23
+ // chain, one hop further down: mqtt-packet's `parser.js` requires the BARE
24
+ // specifier `bl` — mqtt-packet's OWN declared transitive dependency — and it
25
+ // is absent inside hq-cli's bundled node_modules after a torn Windows global
26
+ // install. A bare miss looks like an undeclared dependency, but when the
27
+ // REQUIRING third-party package's own manifest declares it, its absence
28
+ // beside a present requirer can only be a torn install. hq-cli declares
29
+ // nothing about a contract between two third-party packages, so this is never
30
+ // an hq-cli manifest defect.
31
+ //
22
32
  // HQ-CLI-1M (Sentry 7714870912) — ESM load, in-process. A module that existed
23
33
  // at RESOLVE was gone at READ (a concurrent writer rewrote the install tree),
24
34
  // so Node's ESM loader raised `ENOENT` from getSourceSync/readFileSync/openSync
@@ -26,7 +36,7 @@
26
36
  // ERR_MODULE_NOT_FOUND at resolve, never ENOENT at load; an ENOENT at load
27
37
  // proves the file vanished between resolve and read.
28
38
  //
29
- // Both shapes carry no hq-cli frames, reach the boundary's final `else`, and —
39
+ // All shapes carry no hq-cli frames, reach the boundary's final `else`, and —
30
40
  // before this classifier — filed a bare captureException plus an unactionable
31
41
  // `hq: <fallback>` line. The disposition is the one HQ-CLI-Y already
32
42
  // established: an incomplete install is the caller's machine, so the CLI prints
@@ -35,19 +45,26 @@
35
45
  // The gate is deliberately narrow so neither an hq-cli packaging fault nor
36
46
  // user free-text can trip it. Only structured fields are read — `code`,
37
47
  // `syscall`, `path`, `requireStack`, and the stack's loader-frame marker, plus
38
- // the first message line for the CJS specifier. THREE independent narrowings
48
+ // the first message line for the CJS specifier plus, for the bare CJS shape
49
+ // only, the requiring package's own on-disk manifest. Independent narrowings
39
50
  // keep a genuine hq-cli defect reportable:
40
51
  // 1. The failing file must sit under `<packageRoot>/node_modules/` — a
41
52
  // third-party file hq-cli does not author. A miss under `<packageRoot>/dist`
42
53
  // or `/assets` is hq-cli's OWN shipped output and stays captured.
43
- // 2. The CJS shape additionally requires a RELATIVE specifier a
44
- // bare-specifier miss (`Cannot find module 'mqtt'`) can be an undeclared
45
- // dependency (an hq-cli manifest defect) and stays captured.
54
+ // 2. The CJS shape splits on the specifier. A RELATIVE specifier internal to
55
+ // a package under node_modules can only be a truncated copy (HQ-CLI-1N). A
56
+ // BARE PACKAGE-ROOT specifier stays captured UNLESS the requiring
57
+ // third-party package's own manifest declares it in its REQUIRED
58
+ // `dependencies` (HQ-CLI-1Q). A subpath miss (`bl/lib/x`), an
59
+ // optional-dependency miss (not guaranteed installed), and an undeclared or
60
+ // peer-only miss (`Cannot find module 'mqtt'` from a requirer that does not
61
+ // declare it) all stay loud — the last two enriched with both package names.
46
62
  // 3. The ESM shape additionally requires an esm-loader frame — an ordinary
47
63
  // `fs.readFileSync` ENOENT written by hq's own code stays captured.
48
64
  import * as fs from "fs";
49
65
  import * as path from "path";
50
66
  import { packageRoot } from "./hq-roots.js";
67
+ import { packageNameOf } from "./install-tree-torn.js";
51
68
  import { boundedDiagnosticValue } from "./package-root-diagnostics.js";
52
69
  /**
53
70
  * The actionable remedy shown to the operator. Input-free — nothing from the
@@ -72,6 +89,11 @@ const RELATIVE_SPECIFIER = /^\.\.?[\\/]/;
72
89
  const ESM_LOADER_FRAME = /node:internal[\\/]modules[\\/]esm[\\/]/;
73
90
  const ROOT_DIAGNOSTIC_BYTES = 256;
74
91
  const CODE_DIAGNOSTIC_BYTES = 32;
92
+ // Package names live inside hq-cli's own dependency graph, so their universe is
93
+ // bounded; the cap is a belt for a pathological requireStack, not a real limit.
94
+ const PACKAGE_NAME_DIAGNOSTIC_BYTES = 128;
95
+ /** The `<something>/node_modules/<something>` directory-boundary marker. */
96
+ const NODE_MODULES_SEGMENT = "/node_modules/";
75
97
  /**
76
98
  * packageRoot() walks up from the compiled module and THROWS
77
99
  * PackageRootResolutionError when it cannot resolve. This classifier runs inside
@@ -132,6 +154,114 @@ function parseMissingSpecifier(message) {
132
154
  const match = message.match(/Cannot find module ['"]([^'"]+)['"]/);
133
155
  return match ? match[1] : null;
134
156
  }
157
+ /**
158
+ * True when `specifier` is a BARE package specifier — neither a RELATIVE
159
+ * specifier (`./x`, `..\x`) nor an ABSOLUTE POSIX (`/x`) or Windows (`C:\x`,
160
+ * `\\unc`) path. Shape-based (never `process.platform`) so a reported Windows
161
+ * path classifies on a Linux CI runner, mirroring normalizeForCompare's win32
162
+ * detection.
163
+ */
164
+ function isBareSpecifier(specifier) {
165
+ if (specifier.length === 0)
166
+ return false;
167
+ if (RELATIVE_SPECIFIER.test(specifier))
168
+ return false;
169
+ if (specifier.startsWith("/"))
170
+ return false; // absolute POSIX
171
+ if (looksWin32(specifier))
172
+ return false; // absolute Windows (drive letter or UNC)
173
+ return true;
174
+ }
175
+ /**
176
+ * True when `specifier` is a bare specifier naming a PACKAGE ROOT with no
177
+ * subpath — `bl` or `@scope/name`, never `bl/lib/inner.js` or `@scope/name/sub`.
178
+ * Only a WHOLE-package miss is a proven torn install; a subpath miss can mean the
179
+ * package IS installed but that one file is absent because of a version or
180
+ * packaging mismatch — a real defect that must stay reported, not be silenced as
181
+ * a torn install.
182
+ */
183
+ function isBarePackageRoot(specifier) {
184
+ return isBareSpecifier(specifier) && specifier === packageNameOf(specifier);
185
+ }
186
+ /**
187
+ * From a file under `<root>/node_modules/…`, derive the REQUIRING package's
188
+ * directory and its package name, nesting- and scope-aware:
189
+ * `<root>/node_modules/a/node_modules/b/index.js` → dir `…/b`, name `b`
190
+ * `<root>/node_modules/@scope/pkg/index.js` → dir `…/@scope/pkg`, name `@scope/pkg`
191
+ * Keyed on the LAST `node_modules` segment so the INNERMOST (actually requiring)
192
+ * package is chosen, consuming two path segments for an `@scope/name` dir. The
193
+ * returned dir preserves the original case and is folded to `/` (Node's fs
194
+ * accepts `/` on every platform); the win32 shape only case-folds the SEARCH so
195
+ * the segment is found, never the returned value. Returns null when the path
196
+ * holds no usable package segment.
197
+ */
198
+ function requiringPackage(requiringFile) {
199
+ const folded = foldSeparators(requiringFile);
200
+ const win = looksWin32(requiringFile);
201
+ const haystack = win ? folded.toLowerCase() : folded;
202
+ const marker = win ? NODE_MODULES_SEGMENT.toLowerCase() : NODE_MODULES_SEGMENT;
203
+ const lastNm = haystack.lastIndexOf(marker);
204
+ if (lastNm === -1)
205
+ return null;
206
+ const prefixEnd = lastNm + NODE_MODULES_SEGMENT.length;
207
+ const prefix = folded.slice(0, prefixEnd); // `…/node_modules/`, original case
208
+ const rest = folded
209
+ .slice(prefixEnd)
210
+ .split("/")
211
+ .filter((segment) => segment.length > 0);
212
+ if (rest.length === 0)
213
+ return null;
214
+ const take = rest[0].startsWith("@") ? 2 : 1;
215
+ if (rest.length < take)
216
+ return null;
217
+ const name = rest.slice(0, take).join("/");
218
+ return { dir: prefix + name, name };
219
+ }
220
+ /** True when `deps` is an object carrying `packageName` as an OWN key. */
221
+ function declaresDependency(deps, packageName) {
222
+ return (typeof deps === "object" &&
223
+ deps !== null &&
224
+ Object.prototype.hasOwnProperty.call(deps, packageName));
225
+ }
226
+ /**
227
+ * Whether the third-party package that REQUIRED `requiringFile` declares
228
+ * `packageName` in its own REQUIRED `dependencies` — the decisive discriminator
229
+ * for Shape C (HQ-CLI-1Q). A correct install of a package that declares a
230
+ * REQUIRED dependency ALWAYS writes it, so its absence beside a present requirer
231
+ * can only be a torn install. `optionalDependencies` are deliberately EXCLUDED:
232
+ * they are not guaranteed installed (`npm install --omit optional`, or a
233
+ * tolerated optional-install failure), so a missing optional package can be a
234
+ * persistent, expected omission — not a torn install — and must stay captured. A
235
+ * peer-only / undeclared / hoisting assumption is likewise NOT declared here.
236
+ * Fails CLOSED: an unresolved dir, an unreadable or invalid-JSON manifest, or a
237
+ * non-object dependency map all return false. Never throws — every fs and
238
+ * JSON.parse call is individually guarded. Reached only after the cheap
239
+ * in-memory narrowings of Shape C pass, so at most one small manifest read per
240
+ * process.
241
+ */
242
+ function requirerDeclaresDependency(requiringFile, packageName, fileSystem) {
243
+ const requirer = requiringPackage(requiringFile);
244
+ if (!requirer)
245
+ return false;
246
+ let raw;
247
+ try {
248
+ raw = fileSystem.readFileSync(`${requirer.dir}/package.json`, "utf-8");
249
+ }
250
+ catch {
251
+ return false;
252
+ }
253
+ let manifest;
254
+ try {
255
+ manifest = JSON.parse(raw);
256
+ }
257
+ catch {
258
+ return false;
259
+ }
260
+ if (manifest === null || typeof manifest !== "object")
261
+ return false;
262
+ const record = manifest;
263
+ return declaresDependency(record.dependencies, packageName);
264
+ }
135
265
  /** True when `stack` carries a Node ESM loader frame. */
136
266
  function hasEsmLoaderFrame(stack) {
137
267
  return typeof stack === "string" && ESM_LOADER_FRAME.test(stack);
@@ -161,7 +291,7 @@ function isEsmLoaderEnoent(err) {
161
291
  * means "handle as usual (capture to Sentry)". Never throws — a resolver that
162
292
  * fails yields null.
163
293
  */
164
- export function incompleteInstallMessage(err, resolvePackageRoot = safePackageRoot) {
294
+ export function incompleteInstallMessage(err, resolvePackageRoot = safePackageRoot, fileSystem = fs) {
165
295
  if (err === null || typeof err !== "object")
166
296
  return null;
167
297
  const record = err;
@@ -172,16 +302,36 @@ export function incompleteInstallMessage(err, resolvePackageRoot = safePackageRo
172
302
  if (!root)
173
303
  return null;
174
304
  if (code === "MODULE_NOT_FOUND") {
175
- // Shape A (CJS, HQ-CLI-1N): a RELATIVE specifier internal to a package under
176
- // the running install's node_modules can only be a truncated on-disk copy.
177
305
  const requireStack = record.requireStack;
178
306
  if (!Array.isArray(requireStack) || typeof requireStack[0] !== "string") {
179
307
  return null;
180
308
  }
309
+ const requiringFile = requireStack[0];
181
310
  const specifier = parseMissingSpecifier(record.message);
182
- if (specifier === null || !RELATIVE_SPECIFIER.test(specifier))
311
+ if (specifier === null)
312
+ return null;
313
+ // Shape A (CJS, HQ-CLI-1N) — UNCHANGED: a RELATIVE specifier internal to a
314
+ // package under the running install's node_modules can only be a truncated
315
+ // on-disk copy. Kept byte-for-byte so 1N cannot regress.
316
+ if (RELATIVE_SPECIFIER.test(specifier)) {
317
+ return isUnderNodeModules(requiringFile, root)
318
+ ? INCOMPLETE_INSTALL_REMEDY
319
+ : null;
320
+ }
321
+ // Shape C (CJS, HQ-CLI-1Q): a BARE PACKAGE-ROOT specifier missing from UNDER
322
+ // the running install's node_modules is a torn install ONLY when the
323
+ // REQUIRING third-party package's own manifest declares it in its required
324
+ // `dependencies`. A subpath specifier (`bl/lib/inner.js`) is excluded — the
325
+ // package may be present and only that file gone to a version mismatch — and
326
+ // an absolute-path specifier is neither relative nor bare, so both stay
327
+ // captured. The manifest read is the ONLY filesystem access here and is
328
+ // reached only after the cheap in-memory narrowings above — a combination
329
+ // that cannot occur on a healthy run — so a healthy event never touches disk.
330
+ if (!isBarePackageRoot(specifier))
331
+ return null;
332
+ if (!isUnderNodeModules(requiringFile, root))
183
333
  return null;
184
- return isUnderNodeModules(requireStack[0], root)
334
+ return requirerDeclaresDependency(requiringFile, specifier, fileSystem)
185
335
  ? INCOMPLETE_INSTALL_REMEDY
186
336
  : null;
187
337
  }
@@ -197,27 +347,80 @@ export function incompleteInstallMessage(err, resolvePackageRoot = safePackageRo
197
347
  ? INCOMPLETE_INSTALL_REMEDY
198
348
  : null;
199
349
  }
350
+ /** A readFileSync surface, defaulting to the real fs when the caller injects none. */
351
+ function readFileFrom(fileSystem) {
352
+ return { readFileSync: fileSystem.readFileSync ?? fs.readFileSync };
353
+ }
200
354
  /**
201
- * When an esm-loader ENOENT reaches the capture path WITHOUT being suppressed —
202
- * the exact shape the delivered HQ-CLI-1M payload arrived in, where neither the
203
- * exception value nor node_system_error carried a `path` return a bounded
204
- * `contexts.incomplete_install` block so the next occurrence carries the
205
- * evidence this one lacked; otherwise return undefined (bare capture). Built
206
- * with the byte-capped, scrubber-safe discipline of package-root-diagnostics.ts:
207
- * the resolved package root and whether its package.json / node_modules exist,
208
- * the loader-frame marker, and the bounded errno code — never a caller argv,
209
- * query, or user-minted value. Never throws.
210
- *
211
- * main.ts attaches this on the generic capture path exactly as
212
- * qmdSpawnFailureCaptureContext already does.
355
+ * When a third-party BARE-specifier CJS miss UNDER the running install's
356
+ * node_modules reached the capture path WITHOUT being suppressed (HQ-CLI-1Q)
357
+ * i.e. the requiring package did NOT declare it, so it is a possible undeclared
358
+ * or peer-only dependency defect return a bounded `incomplete_install` block
359
+ * naming both packages so the next occurrence is attributable instead of bare;
360
+ * otherwise undefined. The suppression decision is delegated to
361
+ * incompleteInstallMessage against the SAME injected filesystem, so a declared
362
+ * (torn-install) miss is never double-attributed here. Never throws.
363
+ */
364
+ function bareSpecifierCaptureContext(err, resolvePackageRoot, fileSystem) {
365
+ if (err === null || typeof err !== "object")
366
+ return undefined;
367
+ const record = err;
368
+ if (record.code !== "MODULE_NOT_FOUND")
369
+ return undefined;
370
+ const requireStack = record.requireStack;
371
+ if (!Array.isArray(requireStack) || typeof requireStack[0] !== "string")
372
+ return undefined;
373
+ const requiringFile = requireStack[0];
374
+ const specifier = parseMissingSpecifier(record.message);
375
+ // Only a WHOLE-package bare miss is attributed here, matching the classifier's
376
+ // Shape C gate: a subpath miss is a version/packaging defect, not "the package
377
+ // is missing", so naming the package would misattribute it.
378
+ if (specifier === null || !isBarePackageRoot(specifier))
379
+ return undefined;
380
+ const root = resolveRootSafely(resolvePackageRoot);
381
+ if (!root || !isUnderNodeModules(requiringFile, root))
382
+ return undefined;
383
+ // Suppressed (the requirer declares it) → printed-and-skipped, never captured.
384
+ if (incompleteInstallMessage(err, resolvePackageRoot, readFileFrom(fileSystem)) !== null) {
385
+ return undefined;
386
+ }
387
+ return {
388
+ incomplete_install: {
389
+ missingPackage: boundedDiagnosticValue(specifier, PACKAGE_NAME_DIAGNOSTIC_BYTES),
390
+ requiringPackage: boundedDiagnosticValue(requiringPackage(requiringFile)?.name ?? "<unresolved>", PACKAGE_NAME_DIAGNOSTIC_BYTES),
391
+ requirerDeclaresMissing: false,
392
+ },
393
+ };
394
+ }
395
+ /**
396
+ * When an incomplete-install failure reaches the capture path WITHOUT being
397
+ * suppressed, return a bounded `contexts.incomplete_install` block so the next
398
+ * occurrence carries the evidence this one lacked; otherwise return undefined
399
+ * (bare capture). Two enriched shapes:
400
+ * - a third-party BARE-specifier miss the requirer did not declare (HQ-CLI-1Q)
401
+ * → { missingPackage, requiringPackage, requirerDeclaresMissing:false };
402
+ * - an esm-loader ENOENT whose path did not survive delivery (HQ-CLI-1M) — the
403
+ * shape the delivered payload arrived in, where neither the exception value
404
+ * nor node_system_error carried a `path` → { packageRoot, packageJsonExists,
405
+ * nodeModulesExists, esmLoaderFrame, code }.
406
+ * Built with the byte-capped, scrubber-safe discipline of
407
+ * package-root-diagnostics.ts — never a caller argv, query, or user-minted
408
+ * value. Never throws. main.ts attaches this on the generic capture path
409
+ * exactly as qmdSpawnFailureCaptureContext already does.
213
410
  */
214
411
  export function incompleteInstallCaptureContext(err, resolvePackageRoot = safePackageRoot, fileSystem = fs) {
412
+ // (A) HQ-CLI-1Q — the not-suppressed third-party bare miss, made attributable.
413
+ const bare = bareSpecifierCaptureContext(err, resolvePackageRoot, fileSystem);
414
+ if (bare)
415
+ return bare;
416
+ // (B) HQ-CLI-1M — the path-less esm-loader ENOENT, unchanged.
215
417
  if (!isEsmLoaderEnoent(err))
216
418
  return undefined;
217
419
  // Only instrument what we did NOT already confidently suppress: a path under
218
420
  // node_modules is classified and printed above, never captured.
219
- if (incompleteInstallMessage(err, resolvePackageRoot) !== null)
421
+ if (incompleteInstallMessage(err, resolvePackageRoot, readFileFrom(fileSystem)) !== null) {
220
422
  return undefined;
423
+ }
221
424
  const record = err;
222
425
  const root = resolveRootSafely(resolvePackageRoot);
223
426
  const code = typeof record.code === "string" ? record.code : "";
@@ -22,6 +22,9 @@ export interface PersonRecord {
22
22
  name: string;
23
23
  /** Contact email, when recorded. */
24
24
  email?: string;
25
+ /** Canonical identity from authoritative company membership, never local YAML. */
26
+ personUid?: string;
27
+ companyUid?: string;
25
28
  /** "internal" (team member) | "external" (client, vendor, …). */
26
29
  type?: string;
27
30
  /** Role/title inside (or relative to) the company. */
@@ -31,11 +31,6 @@ export interface EntitlementEntry {
31
31
  export interface EntitlementListResponse {
32
32
  entitlements: EntitlementEntry[];
33
33
  }
34
- export interface EntitlementCheckResponse {
35
- entitled: boolean;
36
- tier?: string;
37
- expires_at?: string;
38
- }
39
34
  export interface DownloadResponse {
40
35
  url: string;
41
36
  sha256: string;
@@ -59,7 +54,6 @@ export declare class RegistryClient {
59
54
  }): Promise<PackageListResponse>;
60
55
  getPackage(slug: string): Promise<PackageResponse>;
61
56
  getMyEntitlements(): Promise<EntitlementListResponse>;
62
- checkEntitlement(slug: string): Promise<EntitlementCheckResponse>;
63
57
  getDownloadUrl(slug: string, version?: string): Promise<DownloadResponse>;
64
58
  }
65
59
  //# sourceMappingURL=registry-client.d.ts.map
@@ -88,9 +88,6 @@ export class RegistryClient {
88
88
  auth: true,
89
89
  });
90
90
  }
91
- async checkEntitlement(slug) {
92
- return this.request('GET', `/entitlements/${encodeURIComponent(slug)}`, { auth: true });
93
- }
94
91
  async getDownloadUrl(slug, version) {
95
92
  const params = new URLSearchParams();
96
93
  if (version)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.109.5",
4
- "description": "HQ by Indigo management CLI modules and cloud sync",
3
+ "version": "5.109.7",
4
+ "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "hq": "dist/index.js",
@@ -26,7 +26,8 @@
26
26
  "coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
27
27
  "vitest": "vitest",
28
28
  "clean": "rm -rf dist",
29
- "prepare": "husky || true"
29
+ "prepare": "husky || true",
30
+ "check:no-hermes": "bash scripts/check-no-hermes.sh"
30
31
  },
31
32
  "dependencies": {
32
33
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",