@indigoai-us/hq-cli 5.341.1 → 5.341.3

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 (42) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/assets/hqd/windows-pipe-host.ps1 +282 -0
  3. package/dist/bin/hq-auth-refresh.d.ts +1 -1
  4. package/dist/bin/hq-auth-refresh.js +11 -2
  5. package/dist/commands/billing-sponsor.js +20 -1
  6. package/dist/commands/core-derive-trigger-facts-native.d.ts +2 -0
  7. package/dist/commands/core-derive-trigger-facts-native.js +92 -25
  8. package/dist/commands/core-migrate-policy-triggers-native.d.ts +1 -0
  9. package/dist/commands/core-migrate-policy-triggers-native.js +26 -12
  10. package/dist/commands/doctor.js +2 -2
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +17 -1
  13. package/dist/lib/aws-sdk-node-warning-output.js +2 -3
  14. package/dist/lib/billing/plan-lock.js +19 -2
  15. package/dist/lib/bot/owner-context.d.ts +2 -0
  16. package/dist/lib/bot/owner-context.js +19 -1
  17. package/dist/lib/daemon/flag-monitor.d.ts +23 -0
  18. package/dist/lib/daemon/flag-monitor.js +99 -0
  19. package/dist/lib/daemon/flag.d.ts +2 -0
  20. package/dist/lib/daemon/flag.js +10 -1
  21. package/dist/lib/daemon/protocol.d.ts +26 -3
  22. package/dist/lib/daemon/protocol.js +9 -5
  23. package/dist/lib/daemon/run.js +14 -16
  24. package/dist/lib/daemon/server.d.ts +23 -2
  25. package/dist/lib/daemon/server.js +397 -52
  26. package/dist/lib/daemon/windows-pipe-host.d.ts +17 -0
  27. package/dist/lib/daemon/windows-pipe-host.js +218 -0
  28. package/dist/lib/diagnostics.d.ts +9 -0
  29. package/dist/lib/diagnostics.js +12 -0
  30. package/dist/lib/doctor/checks/anywhere.d.ts +6 -1
  31. package/dist/lib/doctor/checks/anywhere.js +131 -13
  32. package/dist/lib/doctor/json-output.d.ts +2 -0
  33. package/dist/lib/doctor/json-output.js +1 -0
  34. package/dist/lib/doctor/registry.js +1 -3
  35. package/dist/lib/doctor/types.d.ts +4 -0
  36. package/dist/utils/package-use-lease.d.ts +4 -0
  37. package/dist/utils/package-use-lease.js +28 -0
  38. package/dist/utils/run-bundled-script.d.ts +2 -0
  39. package/dist/utils/run-bundled-script.js +10 -1
  40. package/dist/utils/vault-api.js +12 -12
  41. package/docs/anywhere-runtime.md +49 -5
  42. package/package.json +2 -1
@@ -0,0 +1,218 @@
1
+ import { spawn } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import { createInterface } from "node:readline";
4
+ import { Duplex } from "node:stream";
5
+ import { fileURLToPath } from "node:url";
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { parseSddlDacl, sddlAccountToSid } from "../work-context/windows-acl.js";
9
+ export const WINDOWS_PIPE_STARTUP_REFUSAL = "hqd is disabled on Windows because the default named-pipe ACL grants read access to Everyone and Anonymous accounts.";
10
+ export function validateWindowsPipeAcl(report) {
11
+ if (!/^S-1-\d+(?:-\d+)+$/i.test(report.userSid))
12
+ throw new Error("Windows pipe ACL did not report a valid current user SID");
13
+ const aces = parseSddlDacl(report.sddl);
14
+ if (!aces || aces.length !== 1)
15
+ throw new Error("Windows pipe DACL is not user-only");
16
+ const [ace] = aces;
17
+ // Windows uses the LA SDDL alias for the local Administrator account. Its
18
+ // SID is machine-specific, so match it only when the OS-reported current
19
+ // user is that account (RID 500).
20
+ const sid = ace.account.toUpperCase() === "LA" && /^(?:S-1-5-21-\d+-\d+-\d+-)500$/i.test(report.userSid)
21
+ ? report.userSid.toUpperCase()
22
+ : sddlAccountToSid(ace.account);
23
+ const rights = ace.rights.toUpperCase();
24
+ const fullPipeControl = rights === "GA" || rights === "0X1F019F" || rights === "0X001F019F";
25
+ if (ace.type !== "A" || ace.flags !== "" || sid?.toUpperCase() !== report.userSid.toUpperCase() || !fullPipeControl) {
26
+ throw new Error("Windows pipe DACL is not user-only");
27
+ }
28
+ const daclPrefix = report.sddl.match(/(?:^|:)D:([A-Z_]*)\(/i)?.[1] ?? "";
29
+ if (!daclPrefix.includes("P"))
30
+ throw new Error("Windows pipe DACL is not protected from inherited ACEs");
31
+ return report;
32
+ }
33
+ function helperPath() {
34
+ return fileURLToPath(new URL("../../../assets/hqd/windows-pipe-host.ps1", import.meta.url));
35
+ }
36
+ function powershellPath() {
37
+ const root = process.env.SystemRoot ?? process.env.windir ?? "C:\\Windows";
38
+ const windowsPowerShell = path.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
39
+ if (fs.existsSync(windowsPowerShell))
40
+ return windowsPowerShell;
41
+ const programFiles = process.env.ProgramW6432 ?? process.env.ProgramFiles ?? "C:\\Program Files";
42
+ const pwsh7 = path.join(programFiles, "PowerShell", "7", "pwsh.exe");
43
+ if (fs.existsSync(pwsh7))
44
+ return pwsh7;
45
+ throw new Error("Windows PowerShell 5.1 or PowerShell 7 is unavailable");
46
+ }
47
+ class WindowsPipeClient extends Duplex {
48
+ host;
49
+ connectionId;
50
+ constructor(host, connectionId) {
51
+ super({ decodeStrings: true, allowHalfOpen: false });
52
+ this.host = host;
53
+ this.connectionId = connectionId;
54
+ }
55
+ _read() { }
56
+ _write(chunk, _encoding, callback) {
57
+ this.host.write(this.connectionId, chunk, callback);
58
+ }
59
+ _final(callback) {
60
+ this.host.closeConnection(this.connectionId);
61
+ callback();
62
+ }
63
+ _destroy(err, callback) {
64
+ this.host.closeConnection(this.connectionId);
65
+ callback(err);
66
+ }
67
+ receive(data) {
68
+ this.push(data);
69
+ }
70
+ endFromHost() {
71
+ this.push(null);
72
+ }
73
+ }
74
+ class SpawnedWindowsPipeHost extends EventEmitter {
75
+ child;
76
+ clients = new Map();
77
+ lineReader;
78
+ closed = false;
79
+ closing = false;
80
+ constructor(pipePath) {
81
+ super();
82
+ const script = helperPath();
83
+ if (!fs.existsSync(script))
84
+ throw new Error(`Windows pipe helper is missing at ${script}`);
85
+ this.child = spawn(powershellPath(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script], {
86
+ windowsHide: true,
87
+ stdio: ["pipe", "pipe", "pipe"],
88
+ env: { ...process.env, HQD_PIPE_NAME: pipePath },
89
+ });
90
+ let stderr = "";
91
+ this.child.stderr?.setEncoding("utf8");
92
+ this.child.stderr?.on("data", (chunk) => {
93
+ stderr = `${stderr}${chunk}`.slice(-8_192);
94
+ });
95
+ this.lineReader = createInterface({ input: this.child.stdout, crlfDelay: Infinity });
96
+ this.lineReader.on("line", (line) => this.receiveLine(line));
97
+ this.child.once("error", (err) => this.emit("failure", err));
98
+ this.child.once("exit", (code, signal) => {
99
+ this.closed = true;
100
+ if (this.closing)
101
+ return;
102
+ const err = new Error(`Windows pipe helper exited (${signal ?? code})${stderr.trim() ? `: ${stderr.trim()}` : ""}`);
103
+ this.emit("failure", err);
104
+ for (const client of this.clients.values())
105
+ client.destroy(err);
106
+ this.clients.clear();
107
+ });
108
+ }
109
+ static async start(pipePath, timeoutMs = 15_000) {
110
+ const host = new SpawnedWindowsPipeHost(pipePath);
111
+ try {
112
+ const acl = await new Promise((resolve, reject) => {
113
+ const timeout = setTimeout(() => reject(new Error("Windows pipe helper did not verify its ACL before timeout")), timeoutMs);
114
+ const onReady = (report) => {
115
+ clearTimeout(timeout);
116
+ host.off("failure", onFailure);
117
+ resolve(report);
118
+ };
119
+ const onFailure = (err) => {
120
+ clearTimeout(timeout);
121
+ host.off("ready", onReady);
122
+ reject(err);
123
+ };
124
+ host.once("ready", onReady);
125
+ host.once("failure", onFailure);
126
+ });
127
+ try {
128
+ return { host, acl: validateWindowsPipeAcl(acl) };
129
+ }
130
+ catch (err) {
131
+ throw new Error(`Windows pipe helper reported DACL ${JSON.stringify(acl.sddl)} for ${acl.userSid}: ${err.message}`);
132
+ }
133
+ }
134
+ catch (err) {
135
+ await host.close();
136
+ throw err;
137
+ }
138
+ }
139
+ receiveLine(line) {
140
+ const [kind, rawId, rawData] = line.split("\t");
141
+ if (kind === "READY") {
142
+ try {
143
+ this.emit("ready", { userSid: rawId, sddl: Buffer.from(rawData ?? "", "base64").toString("utf8") });
144
+ }
145
+ catch (err) {
146
+ this.emit("failure", err instanceof Error ? err : new Error(String(err)));
147
+ }
148
+ return;
149
+ }
150
+ if (kind === "FATAL") {
151
+ this.emit("failure", new Error(Buffer.from(rawId ?? "", "base64").toString("utf8") || "Windows pipe helper failed"));
152
+ return;
153
+ }
154
+ if (kind === "REJECT") {
155
+ this.emit("rejectedRemoteClient", Buffer.from(rawId ?? "", "base64").toString("utf8"));
156
+ return;
157
+ }
158
+ const connectionId = Number(rawId);
159
+ if (!Number.isSafeInteger(connectionId))
160
+ return;
161
+ if (kind === "OPEN") {
162
+ const client = new WindowsPipeClient(this, connectionId);
163
+ this.clients.set(connectionId, client);
164
+ this.emit("connection", client);
165
+ return;
166
+ }
167
+ const client = this.clients.get(connectionId);
168
+ if (!client)
169
+ return;
170
+ if (kind === "DATA")
171
+ client.receive(Buffer.from(rawData ?? "", "base64"));
172
+ else if (kind === "CLOSE") {
173
+ client.endFromHost();
174
+ this.clients.delete(connectionId);
175
+ }
176
+ }
177
+ write(connectionId, data, callback) {
178
+ if (this.closed || !this.child.stdin?.writable) {
179
+ callback(new Error("Windows pipe helper is closed"));
180
+ return;
181
+ }
182
+ this.child.stdin.write(`WRITE\t${connectionId}\t${data.toString("base64")}\n`, "utf8", callback);
183
+ }
184
+ closeConnection(connectionId) {
185
+ if (!this.closed && this.child.stdin?.writable)
186
+ this.child.stdin.write(`CLOSE\t${connectionId}\n`);
187
+ this.clients.delete(connectionId);
188
+ }
189
+ startAccepting() {
190
+ if (!this.closed && this.child.stdin?.writable)
191
+ this.child.stdin.write("START\n");
192
+ }
193
+ async close() {
194
+ if (this.closed)
195
+ return;
196
+ this.closing = true;
197
+ this.child.stdin?.end();
198
+ await new Promise((resolve) => {
199
+ const timer = setTimeout(() => {
200
+ this.child.kill();
201
+ resolve();
202
+ }, 5_000);
203
+ this.child.once("exit", () => {
204
+ clearTimeout(timer);
205
+ resolve();
206
+ });
207
+ });
208
+ }
209
+ }
210
+ export async function startWindowsPipeHost(pipePath) {
211
+ const prefix = "\\\\.\\pipe\\";
212
+ const pipeName = pipePath.toLowerCase().startsWith(prefix.toLowerCase()) ? pipePath.slice(prefix.length) : pipePath;
213
+ if (!pipeName || pipeName.includes("\\") || pipeName.includes("/")) {
214
+ throw new Error("Windows hqd pipe path is not a local named-pipe name");
215
+ }
216
+ return SpawnedWindowsPipeHost.start(pipeName);
217
+ }
218
+ //# sourceMappingURL=windows-pipe-host.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * True when the person asked for diagnostics.
3
+ *
4
+ * Global `--verbose`, or `HQ_DEBUG` set to `1`, `true`, or `yes` (any case,
5
+ * surrounding spaces ignored). Callers pass `options.env` and `options.argv`
6
+ * when a command injects them; otherwise the process values are used.
7
+ */
8
+ export declare function diagnosticsEnabled(env?: NodeJS.ProcessEnv, argv?: readonly string[]): boolean;
9
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * True when the person asked for diagnostics.
3
+ *
4
+ * Global `--verbose`, or `HQ_DEBUG` set to `1`, `true`, or `yes` (any case,
5
+ * surrounding spaces ignored). Callers pass `options.env` and `options.argv`
6
+ * when a command injects them; otherwise the process values are used.
7
+ */
8
+ export function diagnosticsEnabled(env = process.env, argv = process.argv) {
9
+ const debug = env.HQ_DEBUG?.trim() ?? "";
10
+ return argv.includes("--verbose") || /^(1|true|yes)$/i.test(debug);
11
+ }
12
+ //# sourceMappingURL=diagnostics.js.map
@@ -8,6 +8,7 @@
8
8
  * WARN or FAIL with a remediation command. When anywhere was never installed
9
9
  * on this machine the family reports a single NA line.
10
10
  */
11
+ import { type HqdHealthResult } from "../../daemon/protocol.js";
11
12
  import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
12
13
  export declare const ANYWHERE_FAMILY_ID = "anywhere";
13
14
  export declare const REMEDIATION: {
@@ -27,6 +28,10 @@ export interface AnywhereDeps {
27
28
  env?: NodeJS.ProcessEnv;
28
29
  /** Version hqd reports; rejects when the socket is unreachable. */
29
30
  ping?: () => Promise<string>;
31
+ /** Read-only hqd health op. Test seam; production queries the daemon socket. */
32
+ health?: (cwd: string) => Promise<HqdHealthResult>;
33
+ /** Test seam for the local runtime gate used to keep other checks out of a disabled runtime. */
34
+ runtimeEnabled?: () => boolean;
30
35
  /** The hq-cli version installed on this machine. Default: this CLI's version. */
31
36
  cliVersion?: string;
32
37
  serviceState?: () => DaemonServiceState;
@@ -34,5 +39,5 @@ export interface AnywhereDeps {
34
39
  }
35
40
  export declare function createAnywhereFamily(deps?: AnywhereDeps): CheckFamily;
36
41
  export declare const anywhereFamily: CheckFamily;
37
- export declare function runAnywhereChecks(_context: CheckContext, deps?: AnywhereDeps): Promise<CheckResult[]>;
42
+ export declare function runAnywhereChecks(context: CheckContext, deps?: AnywhereDeps): Promise<CheckResult[]>;
38
43
  //# sourceMappingURL=anywhere.d.ts.map
@@ -18,11 +18,12 @@ import { HQ_MCP_SERVER_NAME, SKILL_LINK_PREFIX, claudeTargets } from "../../inst
18
18
  import { codexTargets } from "../../install/codex-targets.js";
19
19
  import { CHATGPT_MODE } from "../../install/chatgpt-targets.js";
20
20
  import { GROK_MODE } from "../../install/grok-targets.js";
21
- import { pingHqdVersion } from "../../daemon/convergence.js";
21
+ import { HqdClientError, hqdRequest } from "../../daemon/protocol.js";
22
22
  import { daemonPresenceLabel, heartbeatFile, readHeartbeat } from "../../daemon/heartbeat.js";
23
23
  import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "../../daemon/install.js";
24
24
  import { hqdSocketPath } from "../../daemon/protocol.js";
25
25
  import { readRegistry, registryDir, registryFile } from "../../registry/registry.js";
26
+ import { anywhereRuntimeEnabled } from "../../../commands/anywhere-runtime-flag.js";
26
27
  export const ANYWHERE_FAMILY_ID = "anywhere";
27
28
  export const REMEDIATION = {
28
29
  installClaude: "hq install --global --runtime claude",
@@ -38,9 +39,69 @@ export const anywhereFamily = createAnywhereFamily();
38
39
  function result(status, checkId, message, remediation, target) {
39
40
  return { status, checkId: `anywhere.${checkId}`, message, ...(remediation ? { remediation } : {}), ...(target ? { target } : {}) };
40
41
  }
41
- export async function runAnywhereChecks(_context, deps = {}) {
42
+ export async function runAnywhereChecks(context, deps = {}) {
42
43
  const home = deps.home ?? os.homedir();
43
44
  const env = deps.env ?? process.env;
45
+ const socketPath = hqdSocketPath(home, env);
46
+ const cwd = context.cwd ?? process.cwd();
47
+ const runtimeEnabled = (deps.runtimeEnabled ?? anywhereRuntimeEnabled)();
48
+ let health;
49
+ let healthUnsupported = false;
50
+ try {
51
+ if (deps.health) {
52
+ health = await deps.health(cwd);
53
+ }
54
+ else if (deps.ping) {
55
+ const version = await deps.ping();
56
+ health = {
57
+ reachable: true,
58
+ version,
59
+ uptimeMs: null,
60
+ flag: { enabled: runtimeEnabled, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
61
+ resolution: { scope: "personal", company: null, fallbackReason: "no_match" },
62
+ socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
63
+ };
64
+ }
65
+ else {
66
+ health = await hqdRequest("health", { cwd }, { socketPath });
67
+ }
68
+ }
69
+ catch (error) {
70
+ if (error instanceof HqdClientError && error.code === "unknown_op") {
71
+ try {
72
+ let version = null;
73
+ let uptimeMs = null;
74
+ if (deps.ping) {
75
+ version = await deps.ping();
76
+ }
77
+ else {
78
+ const ping = await hqdRequest("ping", {}, { socketPath });
79
+ if (typeof ping.version === "string")
80
+ version = ping.version;
81
+ if (typeof ping.uptimeMs === "number")
82
+ uptimeMs = ping.uptimeMs;
83
+ }
84
+ health = {
85
+ reachable: true,
86
+ version,
87
+ uptimeMs,
88
+ flag: { enabled: null, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
89
+ resolution: { scope: "unknown", company: null, fallbackReason: null },
90
+ socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
91
+ };
92
+ healthUnsupported = true;
93
+ }
94
+ catch {
95
+ health = unreachableHealth(socketPath);
96
+ }
97
+ }
98
+ else {
99
+ health = unreachableHealth(socketPath);
100
+ }
101
+ }
102
+ const healthCheck = runtimeHealthCheck(health, healthUnsupported);
103
+ if (!runtimeEnabled)
104
+ return [healthCheck];
44
105
  const claude = claudeTargets(home);
45
106
  const codex = codexTargets(home);
46
107
  const installed = [claude.hqPointer, claude.stateFile, codex.stateFile].some((p) => fs.existsSync(p));
@@ -48,10 +109,10 @@ export async function runAnywhereChecks(_context, deps = {}) {
48
109
  result("WARN", "chatgpt-mode", `ChatGPT integration mode: ${CHATGPT_MODE}; host connector setup is managed in ChatGPT`, "hq install --global --runtime chatgpt"),
49
110
  result("WARN", "grok-mode", `Grok integration mode: ${GROK_MODE}; host connector setup is managed in Grok`, "hq install --global --runtime grok"),
50
111
  ];
112
+ const out = [healthCheck, ...remoteModes];
51
113
  if (!installed) {
52
- return [...remoteModes, result("NA", "installed", "HQ local anywhere runtime is not installed on this machine", REMEDIATION.installClaude)];
114
+ return [...out, result("NA", "installed", "HQ local anywhere runtime is not installed on this machine", REMEDIATION.installClaude)];
53
115
  }
54
- const out = [...remoteModes];
55
116
  const pluginInstall = readClaudePluginInstall(claude.stateFile);
56
117
  out.push(checkInstallMode(claude.stateFile));
57
118
  out.push(checkPointer(claude.hqPointer));
@@ -63,16 +124,11 @@ export async function runAnywhereChecks(_context, deps = {}) {
63
124
  out.push(result("FAIL", "daemon-installed", `daemon service is installed but not loaded (${service.detail})`, REMEDIATION.daemonRestart));
64
125
  else
65
126
  out.push(result("PASS", "daemon-installed", `daemon service is installed and loaded (${service.detail})`));
66
- const socketPath = hqdSocketPath(home, env);
67
- const ping = deps.ping ?? (() => pingHqdVersion(socketPath, 2_000));
68
- let running = null;
69
- try {
70
- running = await ping();
127
+ const running = health.reachable ? health.version : null;
128
+ if (health.reachable)
71
129
  out.push(result("PASS", "socket", `hqd answered on ${socketPath}`, undefined, socketPath));
72
- }
73
- catch (err) {
74
- out.push(result("FAIL", "socket", `hqd is not reachable: ${err instanceof Error ? err.message : String(err)}`, REMEDIATION.daemonInstall, socketPath));
75
- }
130
+ else
131
+ out.push(result("FAIL", "socket", `hqd is not reachable on ${socketPath}`, REMEDIATION.daemonInstall, socketPath));
76
132
  const cliVersion = deps.cliVersion ?? CLI_VERSION;
77
133
  if (running === null)
78
134
  out.push(result("FAIL", "version", `cannot compare versions: hqd did not answer (hq-cli is ${cliVersion})`, REMEDIATION.daemonInstall));
@@ -96,6 +152,68 @@ export async function runAnywhereChecks(_context, deps = {}) {
96
152
  out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex));
97
153
  return out;
98
154
  }
155
+ function unreachableHealth(socketPath) {
156
+ return {
157
+ reachable: false,
158
+ version: null,
159
+ uptimeMs: null,
160
+ flag: { enabled: null, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
161
+ resolution: { scope: "unknown", company: null, fallbackReason: null },
162
+ socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
163
+ };
164
+ }
165
+ function runtimeHealthCheck(health, healthUnsupported = false) {
166
+ const { flag, resolution, socket } = health;
167
+ const age = flag.lastSuccessfulReadAgeMs === null
168
+ ? "unknown"
169
+ : `${new Intl.NumberFormat("en-US").format(flag.lastSuccessfulReadAgeMs)} ms`;
170
+ const flagLabel = flag.enabled === null ? "unknown" : flag.enabled ? "on" : "off";
171
+ const binding = resolution.scope === "company"
172
+ ? `company ${resolution.company}`
173
+ : resolution.scope === "personal"
174
+ ? `personal (fallback: ${resolution.fallbackReason ?? "unspecified"})`
175
+ : health.reachable && healthUnsupported
176
+ ? "unknown (resident daemon lacks the health operation)"
177
+ : "unknown (daemon unavailable)";
178
+ const socketSecurity = socket.transport === "windows_named_pipe"
179
+ ? "Windows named pipe (mode and owner are not reported)"
180
+ : `mode ${socket.mode ?? "unknown"}; owner matches current user: ${socket.ownerMatchesUser === null ? "unknown" : socket.ownerMatchesUser ? "yes" : "no"}; user-only: ${socket.userOnly === null ? "unknown" : socket.userOnly ? "yes" : "no"}`;
181
+ const unreachableMessage = socket.transport === "windows_named_pipe"
182
+ ? `hqd is not reachable on ${socket.path}; Windows daemon startup is refused because named-pipe ACLs are unsupported`
183
+ : `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
184
+ const socketNotUserOnly = health.reachable && socket.transport === "unix_socket" && (socket.userOnly !== true || socket.ownerMatchesUser !== true);
185
+ const warningNotes = [
186
+ healthUnsupported ? "resident hqd does not support detailed health; restart hqd for full diagnostics" : null,
187
+ socketNotUserOnly ? "socket is not confirmed user-only" : null,
188
+ ].filter((note) => note !== null);
189
+ const message = health.reachable
190
+ ? `hqd reachable; version ${health.version ?? "unknown"}; uptime ${health.uptimeMs === null ? "unknown" : `${new Intl.NumberFormat("en-US").format(health.uptimeMs)} ms`}; runtime flag ${flagLabel}; last successful flag read ${age} ago; held through read error: ${flag.heldThroughReadError ? "yes" : "no"}; cwd binding ${binding}; socket ${socket.path}; ${socketSecurity}${warningNotes.length ? `; ${warningNotes.join("; ")}` : ""}`
191
+ : unreachableMessage;
192
+ const remediation = healthUnsupported || socketNotUserOnly ? REMEDIATION.daemonRestart : undefined;
193
+ return {
194
+ status: health.reachable && warningNotes.length === 0 ? "PASS" : "WARN",
195
+ checkId: "anywhere.runtime-health",
196
+ target: socket.path,
197
+ message,
198
+ ...(remediation ? { remediation } : {}),
199
+ details: {
200
+ reachable: health.reachable,
201
+ version: health.version,
202
+ uptimeMs: health.uptimeMs,
203
+ flagEnabled: flag.enabled,
204
+ lastSuccessfulFlagReadAgeMs: flag.lastSuccessfulReadAgeMs,
205
+ flagHeldThroughReadError: flag.heldThroughReadError,
206
+ resolvedScope: resolution.scope,
207
+ company: resolution.company,
208
+ fallbackReason: resolution.fallbackReason,
209
+ socketPath: socket.path,
210
+ socketTransport: socket.transport,
211
+ socketMode: socket.mode,
212
+ socketOwnerMatchesUser: socket.ownerMatchesUser,
213
+ socketUserOnly: socket.userOnly,
214
+ },
215
+ };
216
+ }
99
217
  function checkInstallMode(file) {
100
218
  try {
101
219
  const state = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -58,6 +58,8 @@ export interface DoctorJsonResult {
58
58
  target?: string;
59
59
  /** Human-readable explanation. */
60
60
  message: string;
61
+ /** Safe structured diagnostic fields, when a check provides them. */
62
+ details?: Record<string, unknown>;
61
63
  /** Remediation hint. Present only when the result carries one. */
62
64
  remediation?: string;
63
65
  /** Structured remediation (US-011). Present on actionable findings only. */
@@ -60,6 +60,7 @@ function toJsonResult(familyId, result, clientHealthReasonCodesEnabledCheckIds)
60
60
  : {}),
61
61
  ...(result.target !== undefined ? { target: result.target } : {}),
62
62
  message: result.message,
63
+ ...(result.details !== undefined ? { details: result.details } : {}),
63
64
  ...(result.remediation !== undefined
64
65
  ? { remediation: result.remediation }
65
66
  : {}),
@@ -27,7 +27,6 @@ import { anywhereFamily } from "./checks/anywhere.js";
27
27
  import { scaffoldCliFloorFamily } from "./checks/scaffold-cli-floor.js";
28
28
  import { fixtureCoverageFamily } from "./fixtures/discover.js";
29
29
  import { checkClaudeWiring } from "./checks/claude-wiring.js";
30
- import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
31
30
  /**
32
31
  * An ordered, id-keyed set of check families. Registration order is preserved
33
32
  * (Map iteration order), and re-registering an id replaces the family in place
@@ -266,8 +265,7 @@ export function createDefaultRegistry() {
266
265
  registry.register(botsFamily);
267
266
  // HQ anywhere (US-012): pointer, registry, hqd service/socket/version,
268
267
  // heartbeat, MCP registrations and skill links. NA when not installed.
269
- if (anywhereRuntimeEnabled())
270
- registry.register(anywhereFamily);
268
+ registry.register(anywhereFamily);
271
269
  return registry;
272
270
  }
273
271
  //# sourceMappingURL=registry.js.map
@@ -35,6 +35,8 @@ export interface CheckResult {
35
35
  target?: string;
36
36
  /** Human-readable one-line explanation of the result. */
37
37
  message: string;
38
+ /** Safe structured fields for machine-readable results. */
39
+ details?: Record<string, unknown>;
38
40
  /** Optional remediation hint (a command or instruction). */
39
41
  remediation?: string;
40
42
  }
@@ -47,6 +49,8 @@ export interface CheckResult {
47
49
  export interface CheckContext {
48
50
  /** Absolute, resolved HQ tree root every check reads from. */
49
51
  hqRoot: string;
52
+ /** Original caller working directory, used only for company/personal binding diagnostics. */
53
+ cwd?: string;
50
54
  /**
51
55
  * The detected host platform (id, and optionally the evidence behind it).
52
56
  * Host-specific live checks — the US-006 runtime probe — key their
@@ -16,6 +16,10 @@ export declare function packageUseUpdateRequestPath(prefix: string, stateDirecto
16
16
  declare function waitForUpdateRequest(requestPath: string, timeoutMs: number): void;
17
17
  export declare function acquirePackageUseLease(prefix: string, hqVersion: string, stateDirectory?: string): PackageUseLease;
18
18
  export declare function acquireCurrentPackageUseLease(): PackageUseLease | null;
19
+ /** True only when a valid update request belongs to a live updater process. */
20
+ export declare function packageUseUpdatePending(prefix: string, stateDirectory?: string): boolean;
21
+ /** True while the desktop updater owns the per-prefix update request. */
22
+ export declare function currentPackageUseUpdatePending(): boolean;
19
23
  /** True while the desktop updater owns the per-prefix update request. */
20
24
  export declare function currentPackageUseUpdateRequested(): boolean;
21
25
  export declare const __test__: {
@@ -197,6 +197,34 @@ export function acquireCurrentPackageUseLease() {
197
197
  }
198
198
  return acquirePackageUseLease(prefix, pkg.version);
199
199
  }
200
+ /** True only when a valid update request belongs to a live updater process. */
201
+ export function packageUseUpdatePending(prefix, stateDirectory) {
202
+ try {
203
+ waitForUpdateRequest(packageUseUpdateRequestPath(prefix, stateDirectory), 0);
204
+ return false;
205
+ }
206
+ catch (error) {
207
+ if (error instanceof PackageUseUpdatePendingError)
208
+ return true;
209
+ throw error;
210
+ }
211
+ }
212
+ /** True while the desktop updater owns the per-prefix update request. */
213
+ export function currentPackageUseUpdatePending() {
214
+ const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
215
+ if (!pkg || isPnpmGlobal(pkg.root) || isBunGlobal(pkg.root))
216
+ return false;
217
+ const prefix = npmPrefix(pkg.root);
218
+ if (!prefix)
219
+ return false;
220
+ if (process.platform !== "win32") {
221
+ const parts = pkg.root.replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
222
+ const nodeModules = parts.lastIndexOf("node_modules");
223
+ if (nodeModules > 0 && parts[nodeModules - 1] !== "lib")
224
+ return false;
225
+ }
226
+ return packageUseUpdatePending(prefix);
227
+ }
200
228
  /** True while the desktop updater owns the per-prefix update request. */
201
229
  export function currentPackageUseUpdateRequested() {
202
230
  const pkg = findCliPackage(path.dirname(fileURLToPath(import.meta.url)));
@@ -51,6 +51,8 @@ export type RunBundledScriptOptions = {
51
51
  hqRoot?: string;
52
52
  /** Extra environment for the child, applied last. */
53
53
  env?: Record<string, string | undefined>;
54
+ /** Convert the script path for Git Bash when the script resolves sibling assets from BASH_SOURCE. */
55
+ msysScriptPath?: boolean;
54
56
  /** Capture stdout and stderr as bytes instead of inheriting those streams. */
55
57
  captureOutput?: boolean;
56
58
  /** Bytes supplied on stdin. With no value, stdin is inherited as before. */
@@ -44,6 +44,14 @@ export function interpreterFor(asset) {
44
44
  const ext = path.extname(asset).toLowerCase();
45
45
  return ext === ".mjs" || ext === ".js" || ext === ".cjs" ? "node" : "bash";
46
46
  }
47
+ /** Git Bash expects drive paths in MSYS form when an interpreter arg is a script path. */
48
+ function scriptPathForBash(script) {
49
+ if (process.platform !== "win32")
50
+ return script;
51
+ const normalized = script.replaceAll("\\", "/");
52
+ const drivePath = normalized.match(/^([A-Za-z]):\/(.*)$/);
53
+ return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
54
+ }
47
55
  function envValue(env, name) {
48
56
  const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
49
57
  return key ? env[key] : undefined;
@@ -176,7 +184,8 @@ export function runBundledScript(opts) {
176
184
  : inputDescriptor !== undefined
177
185
  ? [stdinStdio, "inherit", "inherit"]
178
186
  : "inherit";
179
- result = spawnSync(executable, [script, ...(opts.args ?? [])], {
187
+ const scriptArg = interpreter === "bash" && opts.msysScriptPath ? scriptPathForBash(script) : script;
188
+ result = spawnSync(executable, [scriptArg, ...(opts.args ?? [])], {
180
189
  cwd: opts.cwd,
181
190
  env,
182
191
  stdio,
@@ -95,20 +95,19 @@ function identityLookupTimeoutMs() {
95
95
  /**
96
96
  * Best-effort peek of a 2xx JSON body for plan-limit status (US-016).
97
97
  *
98
- * For ok JSON responses: read the body ONCE, record planLimits (best-effort),
99
- * and return a NEW Response built from the buffered body so callers can still
100
- * call .json()/.text()/.arrayBuffer(). Non-JSON and non-ok responses are
101
- * returned untouched (streamed binary downloads must never be buffered).
98
+ * For ok JSON responses: inspect a clone, record planLimits (best-effort),
99
+ * cancel the unused original tee branch, and return a NEW Response built from
100
+ * the buffered body so callers can still call .json()/.text()/.arrayBuffer().
101
+ * Non-JSON and non-ok responses are returned untouched (streamed binary
102
+ * downloads must never be buffered).
102
103
  *
103
104
  * On inspection errors, return the original response by default to preserve
104
105
  * existing behavior. Secret-load callers opt into rethrowing body read errors
105
106
  * so the load path can classify a broken stream for offline-cache fallback.
106
107
  *
107
- * Why not response.clone()? Cloning tees the undici body stream; when a
108
- * caller never consumes the original Response body (many commands only check
109
- * response.ok/status), the unused tee branch keeps the connection/handle
110
- * referenced and the process (or vitest worker / spawned CLI child) never
111
- * exits on Linux CI. Reading once + re-wrapping fully drains the stream.
108
+ * Cloning tees the undici body stream. Cancel the original branch after the
109
+ * clone has been read so callers can use the buffered replacement without an
110
+ * unread tee branch keeping the connection/handle referenced.
112
111
  */
113
112
  async function peekPlanLimitStatus(response, options = {}) {
114
113
  let bufferedBody;
@@ -123,8 +122,9 @@ async function peekPlanLimitStatus(response, options = {}) {
123
122
  if (!ct.includes('application/json') && !ct.includes('+json')) {
124
123
  return response;
125
124
  }
126
- const buf = await response.arrayBuffer();
125
+ const buf = await response.clone().arrayBuffer();
127
126
  bufferedBody = buf;
127
+ await response.body?.cancel();
128
128
  try {
129
129
  const text = new TextDecoder().decode(buf);
130
130
  try {
@@ -147,8 +147,8 @@ async function peekPlanLimitStatus(response, options = {}) {
147
147
  }
148
148
  catch (err) {
149
149
  // Only secret-load callers need the body-read error for offline fallback.
150
- // Other endpoints retain the best-effort peek behavior only while the
151
- // original body is still readable.
150
+ // Other endpoints retain the best-effort peek behavior. Reading the clone
151
+ // leaves the original body available when inspection fails.
152
152
  if (bufferedBody !== undefined) {
153
153
  // Retry with safe metadata: statusText can itself make Response
154
154
  // construction throw after the original stream has already been drained.