@indigoai-us/hq-cli 5.326.0 → 5.327.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/command-catalog.generated.d.ts +9 -0
  3. package/dist/command-catalog.generated.js +10 -0
  4. package/dist/commands/auth.js +3 -1
  5. package/dist/commands/core-detect-stale-review-base-fast.d.ts +2 -0
  6. package/dist/commands/core-detect-stale-review-base-fast.js +23 -0
  7. package/dist/commands/core-register-project-native.js +1 -1
  8. package/dist/commands/core-repo-run-registry-fast.d.ts +2 -0
  9. package/dist/commands/core-repo-run-registry-fast.js +35 -0
  10. package/dist/commands/core-rrr-worktree-scope-fix-fast-flag.d.ts +7 -0
  11. package/dist/commands/core-rrr-worktree-scope-fix-fast-flag.js +25 -0
  12. package/dist/commands/core-session-journal-native.js +1 -1
  13. package/dist/commands/core-share-suggestion-state-fast.d.ts +2 -0
  14. package/dist/commands/core-share-suggestion-state-fast.js +25 -0
  15. package/dist/commands/logout.js +2 -0
  16. package/dist/commands/mcp-status.js +7 -0
  17. package/dist/fast-core.js +3 -6
  18. package/dist/fast-scaffold.js +5 -1
  19. package/dist/lib/daemon/mcp-endpoint.d.ts +8 -0
  20. package/dist/lib/daemon/mcp-endpoint.js +30 -0
  21. package/dist/lib/daemon/mcp-oauth.d.ts +24 -0
  22. package/dist/lib/daemon/mcp-oauth.js +298 -0
  23. package/dist/lib/daemon/ops/mcp.d.ts +0 -1
  24. package/dist/lib/daemon/ops/mcp.js +31 -31
  25. package/dist/lib/process-flag-company-context-fast.d.ts +4 -0
  26. package/dist/lib/process-flag-company-context-fast.js +20 -0
  27. package/dist/lib/process-flag-company-context-light.d.ts +9 -0
  28. package/dist/lib/process-flag-company-context-light.js +81 -0
  29. package/dist/lib/process-flag-company-context.d.ts +2 -4
  30. package/dist/lib/process-flag-company-context.js +2 -46
  31. package/dist/utils/hq-roots-core.d.ts +130 -0
  32. package/dist/utils/hq-roots-core.js +354 -0
  33. package/dist/utils/hq-roots.d.ts +2 -129
  34. package/dist/utils/hq-roots.js +2 -353
  35. package/dist/utils/package-root-diagnostics.d.ts +5 -36
  36. package/dist/utils/package-root-diagnostics.js +4 -83
  37. package/dist/utils/package-root-resolution-error.d.ts +32 -0
  38. package/dist/utils/package-root-resolution-error.js +79 -0
  39. package/package.json +1 -1
@@ -2,10 +2,7 @@
2
2
  import { spawn } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
- import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
- import { ensureCognitoToken, isMachineIdentity } from "../../../utils/cognito-session.js";
8
- import { DEFAULT_VAULT_API_URL } from "../../../utils/cognito-session.js";
5
+ import { McpEndpointError, resolveHqMcpEndpoint as resolveEndpoint } from "../mcp-endpoint.js";
9
6
  import { ensurePrivateDir, openPrivateAppend } from "../paths.js";
10
7
  export class McpOpError extends Error {
11
8
  code;
@@ -20,28 +17,28 @@ export function jsonResult(value) {
20
17
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: structured };
21
18
  }
22
19
  export const HQ_MCP_HTTP_TIMEOUT_MS = 30_000;
23
- /** Derive the hosted MCP sibling from the same hq-pro URL used by hq-cli. */
24
20
  export function resolveHqMcpEndpoint(env = process.env) {
25
- const apiUrl = env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
26
- let api;
27
21
  try {
28
- api = new URL(apiUrl);
22
+ return resolveEndpoint(env);
29
23
  }
30
- catch {
31
- throw new McpOpError("hq_mcp_config_error", "HQ's configured API URL is invalid; check HQ_VAULT_API_URL.");
32
- }
33
- if (api.protocol !== "https:" || !api.hostname.startsWith("hqapi.")) {
34
- throw new McpOpError("hq_mcp_config_error", "Hosted hq-mcp requires an HTTPS hqapi host in HQ_VAULT_API_URL.");
24
+ catch (error) {
25
+ if (error instanceof McpEndpointError)
26
+ throw new McpOpError(error.code, error.message);
27
+ throw error;
35
28
  }
36
- api.hostname = `hq-mcp.${api.hostname.slice("hqapi.".length)}`;
37
- api.pathname = "/mcp";
38
- api.search = "";
39
- api.hash = "";
40
- return api;
41
29
  }
42
30
  async function defaultMcpToken() {
43
- if (!isMachineIdentity()) {
44
- throw new McpOpError("hq_mcp_auth_required", "This HQ login is not an hq-mcp audience token. Sign in to the hosted connector with its DCR plus PKCE flow; hq login tokens cannot call hq-mcp yet.");
31
+ const { ensureCognitoToken, resolveCognitoTokenSource } = await import("../../../utils/cognito-session.js");
32
+ if (resolveCognitoTokenSource() !== "machine") {
33
+ const { getMcpAudienceToken, McpOAuthError } = await import("../mcp-oauth.js");
34
+ try {
35
+ return await getMcpAudienceToken();
36
+ }
37
+ catch (error) {
38
+ if (error instanceof McpOAuthError)
39
+ throw new McpOpError(error.code, error.message);
40
+ throw new McpOpError("hq_mcp_auth_required", "Hosted MCP sign-in is required. Run `hq mcp login` and retry.");
41
+ }
45
42
  }
46
43
  try {
47
44
  // Send this box's existing machine token. The hosted hq-mcp audience check
@@ -82,12 +79,7 @@ export class HqMcpBridge {
82
79
  if (!tools.some((tool) => tool.name === name)) {
83
80
  throw new McpOpError("hq_mcp_tool_unavailable", `Hosted hq-mcp does not expose ${name}.`);
84
81
  }
85
- try {
86
- return await client.callTool({ name, arguments: args });
87
- }
88
- catch (error) {
89
- throw this.toSafeError(error, token);
90
- }
82
+ return await client.callTool({ name, arguments: args });
91
83
  }, timeoutMs);
92
84
  }
93
85
  close() {
@@ -104,11 +96,17 @@ export class HqMcpBridge {
104
96
  let timeout;
105
97
  let timedOut = false;
106
98
  const abortController = new AbortController();
99
+ let httpErrorClass;
107
100
  try {
101
+ const [clientModule, httpModule] = await Promise.all([
102
+ import("@modelcontextprotocol/sdk/client/index.js"),
103
+ import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
104
+ ]);
105
+ httpErrorClass = httpModule.StreamableHTTPError;
108
106
  token = await this.getToken();
109
107
  if (!token.trim())
110
108
  throw new McpOpError("hq_mcp_auth_required", "An hq-mcp audience token is required.");
111
- transport = new StreamableHTTPClientTransport(this.endpoint, {
109
+ transport = new httpModule.StreamableHTTPClientTransport(this.endpoint, {
112
110
  requestInit: {
113
111
  headers: { Authorization: `Bearer ${token}` },
114
112
  },
@@ -117,7 +115,7 @@ export class HqMcpBridge {
117
115
  signal: init?.signal ? AbortSignal.any([abortController.signal, init.signal]) : abortController.signal,
118
116
  }),
119
117
  });
120
- client = new Client({ name: "hqd", version: "1" }, { capabilities: {} });
118
+ client = new clientModule.Client({ name: "hqd", version: "1" }, { capabilities: {} });
121
119
  this.activeClients.add(client);
122
120
  timeout = setTimeout(() => {
123
121
  timedOut = true;
@@ -133,7 +131,7 @@ export class HqMcpBridge {
133
131
  if (timedOut) {
134
132
  throw new McpOpError("hq_mcp_timeout", `Hosted hq-mcp request did not finish within ${timeoutMs}ms.`);
135
133
  }
136
- throw this.toSafeError(error, token);
134
+ throw this.toSafeError(error, token, httpErrorClass);
137
135
  }
138
136
  finally {
139
137
  if (timeout)
@@ -157,10 +155,12 @@ export class HqMcpBridge {
157
155
  }
158
156
  }
159
157
  }
160
- toSafeError(error, token) {
158
+ toSafeError(error, token, StreamableHTTPErrorClass) {
161
159
  if (error instanceof McpOpError)
162
160
  return error;
163
- const status = error instanceof StreamableHTTPError ? error.code : undefined;
161
+ if (error instanceof McpEndpointError)
162
+ return new McpOpError(error.code, error.message);
163
+ const status = StreamableHTTPErrorClass && error instanceof StreamableHTTPErrorClass ? error.code : undefined;
164
164
  if (status === 401 || status === 403) {
165
165
  return new McpOpError("hq_mcp_auth_required", "Hosted hq-mcp rejected this token. Use the DCR plus PKCE person flow or configure the hq-mcp machine client ID on this box.");
166
166
  }
@@ -0,0 +1,4 @@
1
+ /** Fast scope resolver; it loads manifest and HQ-root code only for slug scopes. */
2
+ import { type ProcessFlagCompanyContext } from "./process-flag-company-context-light.js";
3
+ export declare function resolveFastProcessFlagCompanyContext(env?: NodeJS.ProcessEnv): Promise<ProcessFlagCompanyContext>;
4
+ //# sourceMappingURL=process-flag-company-context-fast.d.ts.map
@@ -0,0 +1,20 @@
1
+ /** Fast scope resolver; it loads manifest and HQ-root code only for slug scopes. */
2
+ import { normalizedCompanyUid, resolveLightProcessFlagCompanyContext, } from "./process-flag-company-context-light.js";
3
+ export async function resolveFastProcessFlagCompanyContext(env = process.env) {
4
+ const context = resolveLightProcessFlagCompanyContext(env);
5
+ let companyUid = context.companyUid;
6
+ if (!companyUid && context.companySlug) {
7
+ const [{ resolveDefaultHqRoot }, { readCompaniesManifestMap }] = await Promise.all([
8
+ import("../utils/hq-root-resolver.js"),
9
+ import("./work-context/repo-remote.js"),
10
+ ]);
11
+ const hqRoot = resolveDefaultHqRoot({ env });
12
+ const manifestUid = readCompaniesManifestMap(hqRoot)?.[context.companySlug]?.cloud_uid;
13
+ companyUid = typeof manifestUid === "string" ? normalizedCompanyUid(manifestUid) : undefined;
14
+ }
15
+ return {
16
+ ...(companyUid ? { companyUid } : {}),
17
+ ...(context.companySlug ? { companySlug: context.companySlug } : {}),
18
+ };
19
+ }
20
+ //# sourceMappingURL=process-flag-company-context-fast.js.map
@@ -0,0 +1,9 @@
1
+ /** Lightweight process company context shared by full and fast flag readers. */
2
+ export interface ProcessFlagCompanyContext {
3
+ companyUid?: string;
4
+ companySlug?: string;
5
+ }
6
+ export declare function normalizedCompanyUid(value: string | undefined): string | undefined;
7
+ /** Resolve explicit, machine, or bound-session company scope without defaults. */
8
+ export declare function resolveLightProcessFlagCompanyContext(env?: NodeJS.ProcessEnv): ProcessFlagCompanyContext;
9
+ //# sourceMappingURL=process-flag-company-context-light.d.ts.map
@@ -0,0 +1,81 @@
1
+ /** Lightweight process company context shared by full and fast flag readers. */
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
6
+ const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7
+ const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
8
+ const DEFAULT_AGENT_IDENTITY_FILE = "/var/lib/hq-agent/identity.json";
9
+ export function normalizedCompanyUid(value) {
10
+ const candidate = value?.trim() ?? "";
11
+ return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
12
+ }
13
+ function normalizedCompanySlug(value) {
14
+ const candidate = value?.trim() ?? "";
15
+ return COMPANY_SLUG_RE.test(candidate) ? candidate : undefined;
16
+ }
17
+ function record(value) {
18
+ return value && typeof value === "object" && !Array.isArray(value)
19
+ ? value
20
+ : undefined;
21
+ }
22
+ function readAgentIdentity(env) {
23
+ const configured = env.HQ_AGENT_IDENTITY_FILE;
24
+ const file = configured === undefined ? DEFAULT_AGENT_IDENTITY_FILE : configured.trim();
25
+ if (!file)
26
+ return undefined;
27
+ try {
28
+ const identity = record(JSON.parse(fs.readFileSync(file, "utf8")));
29
+ const companyUid = normalizedCompanyUid(typeof identity?.companyUid === "string" ? identity.companyUid : undefined);
30
+ const companySlug = normalizedCompanySlug(typeof identity?.companySlug === "string" ? identity.companySlug : undefined);
31
+ return companyUid || companySlug ? {
32
+ ...(companyUid ? { companyUid } : {}),
33
+ ...(companySlug ? { companySlug } : {}),
34
+ } : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ function readSessionCompany(env) {
41
+ const sessionId = env.HQ_SESSION_ID?.trim();
42
+ if (!sessionId || sessionId.includes("..") || !SESSION_ID_RE.test(sessionId))
43
+ return undefined;
44
+ const contextRoot = env.HQ_WORK_CONTEXT_ROOT?.trim() || path.join(os.homedir(), ".hq", "work-context");
45
+ try {
46
+ const sessionPath = path.join(contextRoot, "sessions", sessionId + ".json");
47
+ const session = record(JSON.parse(fs.readFileSync(sessionPath, "utf8")));
48
+ if (session?.sessionId !== sessionId ||
49
+ session.contextStatus !== "bound" ||
50
+ session.companyConfidence === "device_default")
51
+ return undefined;
52
+ const companyUid = normalizedCompanyUid(typeof session.companyUid === "string" ? session.companyUid : undefined);
53
+ const companySlug = normalizedCompanySlug(typeof session.companySlug === "string" ? session.companySlug : undefined);
54
+ return companyUid || companySlug ? {
55
+ ...(companyUid ? { companyUid } : {}),
56
+ ...(companySlug ? { companySlug } : {}),
57
+ } : undefined;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ /** Resolve explicit, machine, or bound-session company scope without defaults. */
64
+ export function resolveLightProcessFlagCompanyContext(env = process.env) {
65
+ const explicitUid = normalizedCompanyUid(env.HQ_COMPANY_UID);
66
+ const explicitSlug = normalizedCompanySlug(env.HQ_COMPANY_SLUG);
67
+ if (explicitUid || explicitSlug) {
68
+ return { companyUid: explicitUid, companySlug: explicitSlug };
69
+ }
70
+ const spawnValue = env.HQ_SPAWN_COMPANY?.trim();
71
+ if (spawnValue && COMPANY_UID_RE.test(spawnValue))
72
+ return { companyUid: spawnValue };
73
+ const spawnSlug = normalizedCompanySlug(spawnValue);
74
+ if (spawnSlug)
75
+ return { companySlug: spawnSlug };
76
+ const identity = readAgentIdentity(env);
77
+ if (identity)
78
+ return identity;
79
+ return readSessionCompany(env) ?? {};
80
+ }
81
+ //# sourceMappingURL=process-flag-company-context-light.js.map
@@ -1,8 +1,6 @@
1
1
  /** Company scope shared by the full flag registry and its fast-path reader. */
2
- export interface ProcessFlagCompanyContext {
3
- companyUid?: string;
4
- companySlug?: string;
5
- }
2
+ import { type ProcessFlagCompanyContext } from "./process-flag-company-context-light.js";
3
+ export type { ProcessFlagCompanyContext } from "./process-flag-company-context-light.js";
6
4
  /** Resolve the same company identity the process-wide flag reader uses. */
7
5
  export declare function resolveProcessFlagCompanyContext(env?: NodeJS.ProcessEnv): ProcessFlagCompanyContext;
8
6
  //# sourceMappingURL=process-flag-company-context.d.ts.map
@@ -1,51 +1,7 @@
1
1
  /** Company scope shared by the full flag registry and its fast-path reader. */
2
- import { readMachineCompanyBinding } from "./work-context/company.js";
3
- import { readSessionState } from "./work-context/state.js";
4
- import { workContextRoot } from "./work-context/paths.js";
5
2
  import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
6
3
  import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
7
- const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
8
- const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
9
- function normalizedCompanyUid(value) {
10
- const candidate = value?.trim() ?? "";
11
- return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
12
- }
13
- function normalizedCompanySlug(value) {
14
- const candidate = value?.trim() ?? "";
15
- return COMPANY_SLUG_RE.test(candidate) ? candidate : undefined;
16
- }
17
- /** Resolve explicit, machine, or bound-session company scope without defaults. */
18
- function processCompanyContext(env = process.env) {
19
- const explicitUid = normalizedCompanyUid(env.HQ_COMPANY_UID);
20
- const explicitSlug = normalizedCompanySlug(env.HQ_COMPANY_SLUG);
21
- if (explicitUid || explicitSlug) {
22
- return { companyUid: explicitUid, companySlug: explicitSlug };
23
- }
24
- const machine = readMachineCompanyBinding(env);
25
- const spawnValue = env.HQ_SPAWN_COMPANY?.trim();
26
- const spawnSlug = normalizedCompanySlug(spawnValue);
27
- const machineContext = spawnValue && COMPANY_UID_RE.test(spawnValue)
28
- ? { companyUid: spawnValue }
29
- : spawnSlug
30
- ? { companySlug: spawnSlug }
31
- : {
32
- companyUid: normalizedCompanyUid(machine?.uid),
33
- companySlug: normalizedCompanySlug(machine?.slug),
34
- };
35
- if (machineContext.companyUid || machineContext.companySlug)
36
- return machineContext;
37
- const sessionId = env.HQ_SESSION_ID?.trim();
38
- if (!sessionId)
39
- return {};
40
- const session = readSessionState(sessionId, workContextRoot(undefined, env));
41
- if (session?.contextStatus !== "bound" || session.companyConfidence === "device_default") {
42
- return {};
43
- }
44
- return {
45
- companyUid: normalizedCompanyUid(session.companyUid),
46
- companySlug: normalizedCompanySlug(session.companySlug),
47
- };
48
- }
4
+ import { normalizedCompanyUid, resolveLightProcessFlagCompanyContext, } from "./process-flag-company-context-light.js";
49
5
  function companyUidForContext(context, env = process.env) {
50
6
  if (context.companyUid)
51
7
  return context.companyUid;
@@ -57,7 +13,7 @@ function companyUidForContext(context, env = process.env) {
57
13
  }
58
14
  /** Resolve the same company identity the process-wide flag reader uses. */
59
15
  export function resolveProcessFlagCompanyContext(env = process.env) {
60
- const context = processCompanyContext(env);
16
+ const context = resolveLightProcessFlagCompanyContext(env);
61
17
  const companyUid = companyUidForContext(context, env);
62
18
  return {
63
19
  ...(companyUid ? { companyUid } : {}),
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Root resolution for CLI-hosted HQ scaffold code.
3
+ *
4
+ * When shell that used to live inside the HQ scaffold moves into this npm
5
+ * package, it loses the one thing it relied on: `BASH_SOURCE`/`$0` pointed at
6
+ * the user's live HQ tree, so "where am I" and "where is HQ" were the same
7
+ * answer. Inside a globally installed package they are three different answers,
8
+ * and conflating any two of them silently corrupts the wrong tree.
9
+ *
10
+ * liveRoot The user's HQ installation — companies/, workspace/, personal/,
11
+ * the installed .claude/ and core/. This is runtime state. Hidden
12
+ * runtime commands operate here.
13
+ *
14
+ * packageRoot This npm package's own directory, where bundled scaffold assets
15
+ * ship. Read-only in practice: a global install may sit in a
16
+ * root-owned prefix. NEVER usable as liveRoot.
17
+ *
18
+ * There is deliberately no third "source root". Release-source tooling — the
19
+ * linters, generators, and validators that rewrite checked-in artifacts — stays
20
+ * scaffold-owned, so nothing here ever needs to resolve an hq-core checkout. See
21
+ * the scaffold-vs-cli-code-ownership policy for why.
22
+ *
23
+ * The invariant that matters most: `resolveLiveRoot` never consults
24
+ * `import.meta.url`. A relocated script asking "where is HQ" must get the live
25
+ * tree, not the package it happens to be bundled in.
26
+ */
27
+ import * as fs from "fs";
28
+ /** Where bundled scaffold assets live, relative to the package root. */
29
+ export declare const BUNDLED_ASSET_DIR: string;
30
+ /**
31
+ * Does this directory look like an HQ tree root?
32
+ *
33
+ * `companies/` is the load-bearing marker — it is the source of truth for
34
+ * tenants and exists in every real install. The second clause tolerates the
35
+ * three shapes a tree can legitimately be in: a full install, a scaffold
36
+ * checkout without a personal overlay, and a personal-only overlay.
37
+ */
38
+ export declare function isHqRoot(dir: string): boolean;
39
+ export type PackageRootFs = Pick<typeof fs, "existsSync" | "readFileSync" | "realpathSync" | "statSync">;
40
+ export type PackageRootResolverOptions = {
41
+ /** Directory containing the currently-running compiled module. */
42
+ moduleDir: string;
43
+ /** Full module path used only in safe diagnostic output. */
44
+ modulePath?: string;
45
+ /** Injectable filesystem seam for resolver tests. */
46
+ fs?: PackageRootFs;
47
+ };
48
+ /**
49
+ * Make a resolver for this package's root. It caches only a successful lookup:
50
+ * a concurrently replaced manifest must be retried by the next command, not
51
+ * converted into a permanent process failure.
52
+ */
53
+ export declare function createPackageRootResolver(options: PackageRootResolverOptions): () => string;
54
+ /**
55
+ * This package's installed root — the directory holding its `package.json`.
56
+ *
57
+ * Walks up from the compiled module (`dist/utils/hq-roots.js`) until it finds a
58
+ * `package.json` whose name matches, rather than hard-coding `..` hops, so the
59
+ * answer survives a change in output layout.
60
+ */
61
+ export declare function packageRoot(): string;
62
+ /**
63
+ * Derive this package's installed root from the running module's OWN path using
64
+ * STRING operations only — no `readFileSync`, `realpathSync`, or `existsSync`.
65
+ *
66
+ * {@link packageRoot} must read `<dir>/package.json` and `realpathSync` the
67
+ * module directory to answer, so it THROWS in exactly the situation that most
68
+ * needs an answer: a concurrent global install has renamed the installed
69
+ * package directory aside (`.hq-cli-<rand>`) and is re-extracting it file by
70
+ * file, so both the manifest walk and the dist-owner fallback fail on a
71
+ * directory that is momentarily gone. This walks the compiled module's
72
+ * ancestors as strings and returns the DEEPEST one whose trailing segments are
73
+ * `node_modules/@indigoai-us/hq-cli`, which stays correct while that directory
74
+ * does not exist on disk.
75
+ *
76
+ * It is a FALLBACK only — {@link packageRoot} validates the manifest name and
77
+ * is authoritative whenever it succeeds. Returns null for a dev checkout or any
78
+ * layout without that segment triple, so a caller that falls back to it can
79
+ * only ever degrade to today's behaviour (no resolution), never widen it.
80
+ *
81
+ * The deepest match is deliberate: a nested `node_modules/@indigoai-us/hq-cli`
82
+ * inside another package resolves to ITSELF (the copy the module belongs to),
83
+ * never an outer decoy. Case is folded only for a Windows-shaped path, matching
84
+ * the POSIX case-sensitivity the rest of this module assumes.
85
+ */
86
+ export declare function stringDerivedPackageRoot(moduleFilePath?: string): string | null;
87
+ export type LiveRootOptions = {
88
+ /** Explicit `--hq-root` value. Highest precedence. */
89
+ hqRoot?: string;
90
+ /** Start the walk-up here instead of `process.cwd()`. Tests only. */
91
+ cwd?: string;
92
+ /** Environment to read. Tests only. */
93
+ env?: NodeJS.ProcessEnv;
94
+ };
95
+ /**
96
+ * Resolve the user's live HQ installation.
97
+ *
98
+ * Precedence: `--hq-root` → `HQ_ROOT` → `CLAUDE_PROJECT_DIR` → walk up from cwd.
99
+ *
100
+ * ## An explicit root is trusted; a discovered one must be recognized
101
+ *
102
+ * The FIRST explicit candidate present wins outright — there is no fall-through
103
+ * past it, because silently operating on a different tree than the caller named
104
+ * is the worst available outcome.
105
+ *
106
+ * That explicit candidate is checked only for the two things that are always
107
+ * wrong: it must be a directory, and it must not be inside {@link packageRoot}
108
+ * (that is this package, not anyone's HQ). It is deliberately NOT required to
109
+ * satisfy {@link isHqRoot}.
110
+ *
111
+ * Requiring markers here was tried and was wrong: the scripts these roots feed
112
+ * accepted any `HQ_ROOT` before they moved, and several callers legitimately pass
113
+ * a minimal tree — e.g. `rebuild-threads-index` against a fixture holding nothing
114
+ * but `workspace/threads/`. Enforcing markers on an explicit value turned a
115
+ * relocation into a silent contract change and broke those callers.
116
+ *
117
+ * The cwd walk is the opposite case: there the marker check is the only thing
118
+ * that can identify a root at all, so it stays mandatory.
119
+ */
120
+ export declare function resolveLiveRoot(opts?: LiveRootOptions): string;
121
+ /**
122
+ * Absolute path to a scaffold asset bundled in this package.
123
+ *
124
+ * Refuses to resolve outside the asset directory so a manifest entry can never
125
+ * be coaxed into executing an arbitrary path, and fails loudly when the asset
126
+ * is missing — an absent asset means the packlist dropped it, which is a
127
+ * packaging bug worth surfacing rather than a runtime fallback.
128
+ */
129
+ export declare function resolveBundledAsset(relativePath: string): string;
130
+ //# sourceMappingURL=hq-roots-core.d.ts.map