@indigoai-us/hq-cli 5.341.0 → 5.341.2

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,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.341.2] — 2026-10-03
6
+
7
+ - Preserve the original integrations response read error instead of returning a consumed response.
8
+
9
+ - The CLI no longer starts while a confirmed Desktop update is in progress after a lease I/O error.
10
+ - hqd refreshes `hq-anywhere-runtime` every 30 seconds, fails closed when the last successful read is older than 60 seconds, and clears its in-memory stdin login when the flag turns off.
11
+ - Hardened hqd company resolution so unreadable registries and registry links to companies missing from the configured HQ manifest fall back to personal context. Added adversarial coverage for symlinks, nested repositories, clones, path normalization, worktrees, and session bindings.
12
+
13
+ ## [5.341.1] — 2026-10-03
14
+ - Match native policy frontmatter and report bytes to the bundled shell on Windows, including paths, diagnostics, and JSON newlines.
15
+ - Keep session-journal printable paths byte-compatible with the shell on Windows.
16
+
5
17
  ## [5.341.0] — 2026-10-03
6
18
  - Keep-alive reconciliation polls every 15 seconds and admits due lane resumes within the same sweep.
7
19
  - Add flag-gated `hq daemon login --stdin` for machine sign-in on disposable hosts. Credentials are piped over the local daemon socket, and the resulting token stays in hqd memory until restart or token expiry. Login is deadline-bounded, and the generic argv-based `hq daemon call` command cannot submit credentials.
@@ -183,7 +183,7 @@ done > "$TMP/rows.tsv"
183
183
  if [ "$JSON" = 1 ]; then
184
184
  jq -R -s -c --argjson cand "$CAND" '
185
185
  split("\n") | map(select(length>0) | split("\t") | {id:.[0], path:.[1], enforcement:.[2], age_days:(.[3]|tonumber), fired:(.[4]|tonumber), last_fired:(if .[5]=="-" then null else .[5] end), stale_refs:(if .[7]=="" then [] else (.[7]|split(",")) end), single_incident:(.[8]=="1"), candidate_classes:(if .[9]=="" then [] else (.[9]|split(",")) end), retrieved:((.[10] // "0")|tonumber), refs_judged:((.[11] // "0")|tonumber), renamed_refs:(if (.[12] // "")=="" then [] else (.[12]|split(",")) end)})
186
- | if $cand == 1 then map(select(.candidate_classes|length>0)) else . end' "$TMP/rows.tsv"
186
+ | if $cand == 1 then map(select(.candidate_classes|length>0)) else . end' "$TMP/rows.tsv" | tr -d '\r'
187
187
  exit 0
188
188
  fi
189
189
  total="$(wc -l < "$TMP/rows.tsv" | tr -d ' ')"
@@ -18,6 +18,6 @@ type RefreshSession = (options?: {
18
18
  minValiditySeconds?: number;
19
19
  }) => ReturnType<typeof refreshCachedSession>;
20
20
  export declare function runAuthRefresh(refreshSession?: RefreshSession): Promise<number>;
21
- export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
21
+ export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease, updateRequestPending?: () => boolean): Promise<number>;
22
22
  export {};
23
23
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -13,7 +13,7 @@
13
13
  // MUST be first: guard the Node version before any dependency that needs a
14
14
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
15
15
  import "../node-preflight.js";
16
- import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
16
+ import { acquireCurrentPackageUseLease, currentPackageUseUpdatePending, } from "../utils/package-use-lease.js";
17
17
  import "../node-network-compat.js";
18
18
  import { isMainModule } from "../utils/is-main-module.js";
19
19
  const MIN_VALIDITY_FLAG = "--min-validity-seconds";
@@ -93,7 +93,7 @@ function packageUseErrorClass(error) {
93
93
  }
94
94
  return typeof value.name === "string" ? value.name : "unknown";
95
95
  }
96
- export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease) {
96
+ export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease, updateRequestPending = currentPackageUseUpdatePending) {
97
97
  try {
98
98
  acquireLease();
99
99
  }
@@ -102,6 +102,15 @@ export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLe
102
102
  process.stderr.write(`hq-auth-refresh: ${error instanceof Error ? error.message : "The HQ CLI package is being updated"}\n`);
103
103
  return 1;
104
104
  }
105
+ try {
106
+ if (updateRequestPending()) {
107
+ process.stderr.write("hq-auth-refresh: The HQ CLI package is being updated; retry after the update finishes\n");
108
+ return 1;
109
+ }
110
+ }
111
+ catch {
112
+ // A failed pending-request probe preserves the existing fail-open behavior.
113
+ }
105
114
  process.stderr.write(`hq-auth-refresh: package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
106
115
  }
107
116
  const { initSentry } = await import("../sentry.js");
@@ -145,10 +145,11 @@ function resolveSessionId(root, deps) {
145
145
  }
146
146
  function findMetaValue(contents, key) {
147
147
  for (const line of contents.split("\n")) {
148
- const first = line.match(/^\s*(\S+)/)?.[1] ?? "";
148
+ const record = process.platform === "win32" ? line.replace(/\r$/, "") : line;
149
+ const first = record.match(/^\s*(\S+)/)?.[1] ?? "";
149
150
  if (first !== `${key}:`)
150
151
  continue;
151
- return { found: true, value: line.replace(/^[^:]+:[\t ]*/, "").replace(/^"|"$/g, "") };
152
+ return { found: true, value: record.replace(/^[^:]+:[\t ]*/, "").replace(/^"|"$/g, "") };
152
153
  }
153
154
  return { found: false, value: "" };
154
155
  }
@@ -39,7 +39,10 @@ export function runPolicyFrontmatter(args, cwd, pathValue = process.env.PATH, pl
39
39
  while (start < source.length) {
40
40
  const end = source.indexOf(0x0a, start);
41
41
  const lineEnd = end < 0 ? source.length : end;
42
- const line = source.subarray(start, lineEnd);
42
+ const rawLine = source.subarray(start, lineEnd);
43
+ const line = platform === 'win32' && rawLine.at(-1) === 0x0d
44
+ ? rawLine.subarray(0, rawLine.length - 1)
45
+ : rawLine;
43
46
  if (line.length === 3 && line[0] === 0x2d && line[1] === 0x2d && line[2] === 0x2d) {
44
47
  markers += 1;
45
48
  }
@@ -378,12 +378,12 @@ function bashInteger(value) {
378
378
  const parsed = BigInt(value);
379
379
  return parsed < BASH_INTEGER_MIN || parsed > BASH_INTEGER_MAX ? undefined : parsed;
380
380
  }
381
- function bashTestGreaterOrEqual(left, right, scriptPath, line) {
381
+ function bashTestGreaterOrEqual(left, right, scriptPath, line, platform) {
382
382
  const parsed = bashInteger(right);
383
383
  if (parsed === undefined) {
384
384
  return {
385
385
  value: false,
386
- stderr: `${scriptPath}: line ${line}: [: ${right}: integer expression expected\n`,
386
+ stderr: `${scriptPath}: line ${line}: [: ${right}: ${platform === 'win32' ? 'integer expected' : 'integer expression expected'}\n`,
387
387
  };
388
388
  }
389
389
  return { value: BigInt(left) >= parsed, stderr: '' };
@@ -448,13 +448,13 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
448
448
  const singleIncident = ['user-correction', 'session-learning', 'back-pressure-failure'].includes(source) && (version || '1') === '1';
449
449
  const classes = [];
450
450
  if (fired === 0) {
451
- const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 173);
451
+ const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 173, platform);
452
452
  stderr += comparison.stderr;
453
453
  if (comparison.value && usage.anyFiles)
454
454
  classes.push('never-fired');
455
455
  }
456
456
  if (fired > 0) {
457
- const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 174);
457
+ const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 174, platform);
458
458
  stderr += comparison.stderr;
459
459
  if (comparison.value)
460
460
  classes.push('dormant');
@@ -464,14 +464,12 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
464
464
  if (refs.renamed.length > 0)
465
465
  classes.push('renamed-refs');
466
466
  if (singleIncident) {
467
- const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 178);
467
+ const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 178, platform);
468
468
  stderr += comparison.stderr;
469
469
  if (comparison.value && fired <= 1)
470
470
  classes.push('single-incident-old');
471
471
  }
472
- let displayPath = file.startsWith(`${root}/`) ? file.slice(root.length + 1) : file;
473
- if (!path.isAbsolute(file) && !file.startsWith(`${root}/`))
474
- displayPath = file;
472
+ const displayPath = displayPolicyPath(file, root);
475
473
  rows.push({
476
474
  id,
477
475
  path: displayPath,
@@ -648,7 +646,7 @@ function setPolicyStatus(filePath, status, reason, by, env, now, cwd = process.c
648
646
  const trimmedDiagnostic = diagnostic.replace(/\n+$/, '');
649
647
  return {
650
648
  ok: false,
651
- error: `policy-retire: refusing to write ${filePath}; canonical policy validation failed:\n${trimmedDiagnostic}\n`,
649
+ error: `policy-retire: refusing to write ${shellPolicyPath(filePath, env.HQ_ROOT || cwd)}; canonical policy validation failed:\n${trimmedDiagnostic}\n`,
652
650
  };
653
651
  }
654
652
  const tempPath = `${resolvedPath}.tmp`;
@@ -657,7 +655,20 @@ function setPolicyStatus(filePath, status, reason, by, env, now, cwd = process.c
657
655
  return { ok: true };
658
656
  }
659
657
  function displayPolicyPath(filePath, root) {
660
- return filePath.startsWith(`${root}/`) ? filePath.slice(root.length + 1) : filePath;
658
+ if (!path.isAbsolute(filePath))
659
+ return filePath;
660
+ const relative = path.relative(root, filePath);
661
+ if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
662
+ return filePath;
663
+ return relative.split(path.sep).join('/');
664
+ }
665
+ function shellPolicyPath(filePath, root) {
666
+ if (process.platform !== 'win32' || !path.isAbsolute(filePath))
667
+ return filePath;
668
+ const relative = path.relative(root, filePath);
669
+ if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
670
+ return filePath;
671
+ return `${root}/${relative.split(path.sep).join('/')}`;
661
672
  }
662
673
  function resolvePolicyTarget(target, root, cwd) {
663
674
  let found = isFile(path.isAbsolute(target) ? target : path.resolve(cwd, target))
@@ -817,7 +828,7 @@ function autoRetire(options, root, cwd, env, now) {
817
828
  wouldRetire += 1;
818
829
  continue;
819
830
  }
820
- const updated = setPolicyStatus(resolvedForRead, 'retired', `auto: ${why}`, options.by, env, now);
831
+ const updated = setPolicyStatus(resolvedForRead, 'retired', `auto: ${why}`, options.by, env, now, cwd);
821
832
  if (updated.ok) {
822
833
  stdout += `retired: ${candidate.id} — ${why}\n`;
823
834
  reportText += `- **${candidate.id}** (${candidate.path}): ${why}\n`;
@@ -875,7 +886,7 @@ function fromReport(options, root, cwd, env, now) {
875
886
  let selected = 0;
876
887
  let stdout = '';
877
888
  let stderr = report === null
878
- ? `jq: error (at ${reportPath}:0): Cannot iterate over null (null)\n`
889
+ ? `jq: error (at ${reportPath}:0): Cannot iterate over null (null)${process.platform === 'win32' ? '\r\n' : '\n'}`
879
890
  : '';
880
891
  for (const value of rows) {
881
892
  if (value === null || typeof value !== 'object' || Array.isArray(value))
@@ -55,6 +55,10 @@ function yamlDoubleQuoted(value) {
55
55
  }
56
56
  const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
57
57
  const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
58
+ // The shell companion builds printable paths by appending slash-delimited
59
+ // suffixes to HQ_ROOT. Keep those bytes on Windows while filesystem access
60
+ // continues to use the host path API.
61
+ const printableJournalDir = (root, date) => path.posix.join(root, "workspace/threads/journal", date);
58
62
  const sessionJournalAsset = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/scaffold/core/scripts/session-journal.sh");
59
63
  const hasFile = (file) => {
60
64
  try {
@@ -182,7 +186,9 @@ function options(args, valueFlags) {
182
186
  function runToolCounter(root, action, sessionArg, env, now, writers) {
183
187
  const rawKey = sessionArg || env.HQ_JOURNAL_SESSION || env.HQ_HOOK_SESSION_ID || "";
184
188
  const key = counterKey(rawKey);
185
- const dir = journalDir(root, now().toISOString().slice(0, 10));
189
+ const date = now().toISOString().slice(0, 10);
190
+ const dir = journalDir(root, date);
191
+ const printableCounter = `${printableJournalDir(root, date)}/.tool-count-${key}`;
186
192
  try {
187
193
  fs.mkdirSync(dir, { recursive: true });
188
194
  }
@@ -196,7 +202,7 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
196
202
  throw new Error("Bundled asset is missing from the installed package: assets/scaffold/core/scripts/session-journal.sh\n" +
197
203
  "This is a packaging fault — the file exists in source but was not published.");
198
204
  }
199
- writers.stderr(`${sessionJournalAsset}: line ${line}: ${counter}: Is a directory\n`);
205
+ writers.stderr(`${sessionJournalAsset}: line ${line}: ${printableCounter}: Is a directory\n`);
200
206
  };
201
207
  if (action === "increment") {
202
208
  withCounterLock(lock, () => {
@@ -270,7 +276,7 @@ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
270
276
  fs.mkdirSync(dir, { recursive: true });
271
277
  }
272
278
  catch {
273
- warning(`write: mkdir failed: ${dir}`);
279
+ warning(`write: mkdir failed: ${printableJournalDir(root, date)}`);
274
280
  return 0;
275
281
  }
276
282
  const slug = slugify(title) || "entry";
@@ -289,7 +295,8 @@ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
289
295
  try {
290
296
  for (let attempt = 0; attempt < 10_000; attempt += 1) {
291
297
  seq = nextSequence(dir);
292
- output = path.join(dir, `${seq}-${slug}.md`);
298
+ output = `${printableJournalDir(root, date)}/${seq}-${slug}.md`;
299
+ const outputFile = path.join(dir, `${seq}-${slug}.md`);
293
300
  const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${yamlDoubleQuoted(title)}\n`, `slug: ${yamlSlug}\n`];
294
301
  if (files) {
295
302
  lines.push("files:\n");
@@ -299,7 +306,7 @@ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
299
306
  }
300
307
  lines.push("status: closed\n", "---\n\n", `# ${seq} — ${title}\n\n`);
301
308
  try {
302
- fs.writeFileSync(output, lines.join("") + body, { flag: "wx" });
309
+ fs.writeFileSync(outputFile, lines.join("") + body, { flag: "wx" });
303
310
  created = true;
304
311
  break;
305
312
  }
@@ -341,11 +348,11 @@ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
341
348
  const date = options(afterSequence, ["--date"]).get("--date") || today();
342
349
  const dir = journalDir(root, date);
343
350
  if (command === "dir-path") {
344
- writers.stdout(dir);
351
+ writers.stdout(printableJournalDir(root, date));
345
352
  return 0;
346
353
  }
347
354
  if (command === "index-path") {
348
- writers.stdout(`${dir}/INDEX.md\n`);
355
+ writers.stdout(`${printableJournalDir(root, date)}/INDEX.md\n`);
349
356
  return 0;
350
357
  }
351
358
  if (command === "list") {
package/dist/index.d.ts CHANGED
@@ -4,8 +4,9 @@ import "./node-network-compat-fast.js";
4
4
  import { isVersionRequest } from "./version-request.js";
5
5
  import { isFastCoreRequest } from "./fast-scaffold.js";
6
6
  type PackageUseLeaseLoader = () => Promise<void>;
7
+ type PackageUseUpdateProbe = () => Promise<boolean>;
7
8
  declare function isNpmPackageEntry(entryUrl: string): boolean;
8
- declare function dispatchWithPackageUseLease(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
9
+ declare function dispatchWithPackageUseLease(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader, updateRequestPending?: PackageUseUpdateProbe): void | Promise<void>;
9
10
  declare function runCliEntry(entryUrl: string, dispatch: () => void, loadLease?: PackageUseLeaseLoader): void | Promise<void>;
10
11
  export declare const __test__: {
11
12
  isVersionRequest: typeof isVersionRequest;
package/dist/index.js CHANGED
@@ -140,14 +140,30 @@ function packageUseErrorClass(error) {
140
140
  function dispatchWithPackageUseLease(entryUrl, dispatch, loadLease = async () => {
141
141
  const { acquireCurrentPackageUseLease } = await import("./utils/package-use-lease.js");
142
142
  acquireCurrentPackageUseLease();
143
+ }, updateRequestPending = async () => {
144
+ const { currentPackageUseUpdatePending } = await import("./utils/package-use-lease.js");
145
+ return currentPackageUseUpdatePending();
143
146
  }) {
144
147
  if (!isNpmPackageEntry(entryUrl)) {
145
148
  dispatch();
146
149
  return;
147
150
  }
148
- return loadLease().then(dispatch, (error) => {
151
+ return loadLease().then(dispatch, async (error) => {
149
152
  if (isPackageUseUpdatePendingError(error))
150
153
  throw error;
154
+ try {
155
+ if (await updateRequestPending()) {
156
+ const pending = new Error("The HQ CLI package is being updated; retry after the update finishes");
157
+ pending.name = "PackageUseUpdatePendingError";
158
+ throw pending;
159
+ }
160
+ }
161
+ catch (probeError) {
162
+ if (isPackageUseUpdatePendingError(probeError))
163
+ throw probeError;
164
+ // A lease probe failure must not make the CLI unavailable. Keep the
165
+ // existing fail-open path when no live update request could be confirmed.
166
+ }
151
167
  process.stderr.write(`hq-cli package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
152
168
  dispatch();
153
169
  });
@@ -0,0 +1,18 @@
1
+ export declare const HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS = 30000;
2
+ export declare const HQ_ANYWHERE_FLAG_MAX_STALENESS_MS = 60000;
3
+ export interface BoundedDaemonFlagMonitorOptions {
4
+ read: () => Promise<boolean>;
5
+ now?: () => number;
6
+ onChange?: (enabled: boolean) => void;
7
+ onReadError?: (error: unknown) => void;
8
+ }
9
+ export interface BoundedDaemonFlagMonitor {
10
+ enabled(): Promise<boolean>;
11
+ close(): void;
12
+ }
13
+ /**
14
+ * Refreshes a long-running daemon's flag independently of incoming requests.
15
+ * A failed read keeps the last successful decision only until its 60s age limit.
16
+ */
17
+ export declare function createBoundedDaemonFlagMonitor(options: BoundedDaemonFlagMonitorOptions): BoundedDaemonFlagMonitor;
18
+ //# sourceMappingURL=flag-monitor.d.ts.map
@@ -0,0 +1,86 @@
1
+ export const HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS = 30_000;
2
+ export const HQ_ANYWHERE_FLAG_MAX_STALENESS_MS = 60_000;
3
+ /**
4
+ * Refreshes a long-running daemon's flag independently of incoming requests.
5
+ * A failed read keeps the last successful decision only until its 60s age limit.
6
+ */
7
+ export function createBoundedDaemonFlagMonitor(options) {
8
+ const now = options.now ?? Date.now;
9
+ let value;
10
+ let lastSuccessAt;
11
+ let lastAttemptAt;
12
+ let successVersion = 0;
13
+ let refreshInFlight;
14
+ let expiryTimer;
15
+ let closed = false;
16
+ const publish = (next) => {
17
+ const changed = value !== next;
18
+ value = next;
19
+ if (changed)
20
+ options.onChange?.(next);
21
+ };
22
+ const expireIfStale = () => {
23
+ if (!closed && lastSuccessAt !== undefined && now() - lastSuccessAt >= HQ_ANYWHERE_FLAG_MAX_STALENESS_MS) {
24
+ publish(false);
25
+ }
26
+ };
27
+ const refresh = () => {
28
+ if (closed)
29
+ return Promise.resolve(false);
30
+ if (refreshInFlight)
31
+ return refreshInFlight;
32
+ lastAttemptAt = now();
33
+ refreshInFlight = Promise.resolve().then(options.read).then((next) => {
34
+ if (closed)
35
+ return false;
36
+ lastSuccessAt = now();
37
+ const thisSuccessVersion = ++successVersion;
38
+ publish(next);
39
+ if (expiryTimer)
40
+ clearTimeout(expiryTimer);
41
+ // Expire from the elapsed timer itself, not a wall-clock comparison.
42
+ expiryTimer = setTimeout(() => {
43
+ if (!closed && successVersion === thisSuccessVersion)
44
+ publish(false);
45
+ }, HQ_ANYWHERE_FLAG_MAX_STALENESS_MS);
46
+ expiryTimer.unref?.();
47
+ return next;
48
+ }).catch((error) => {
49
+ options.onReadError?.(error);
50
+ expireIfStale();
51
+ return value ?? false;
52
+ }).finally(() => {
53
+ refreshInFlight = undefined;
54
+ });
55
+ return refreshInFlight;
56
+ };
57
+ const refreshTimer = setInterval(() => {
58
+ expireIfStale();
59
+ void refresh();
60
+ }, HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS);
61
+ refreshTimer.unref?.();
62
+ void refresh();
63
+ return {
64
+ async enabled() {
65
+ expireIfStale();
66
+ if (lastSuccessAt !== undefined && now() - lastSuccessAt >= HQ_ANYWHERE_FLAG_MAX_STALENESS_MS) {
67
+ void refresh();
68
+ return false;
69
+ }
70
+ if (value === undefined)
71
+ return refresh();
72
+ if (lastAttemptAt === undefined || now() - lastAttemptAt >= HQ_ANYWHERE_FLAG_REFRESH_INTERVAL_MS) {
73
+ void refresh();
74
+ }
75
+ return value;
76
+ },
77
+ close() {
78
+ closed = true;
79
+ if (refreshTimer)
80
+ clearInterval(refreshTimer);
81
+ if (expiryTimer)
82
+ clearTimeout(expiryTimer);
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=flag-monitor.js.map
@@ -20,6 +20,8 @@ export interface ReadDaemonFlagOptions {
20
20
  createClient?: (options: FlagClientOptions) => DaemonFlagClient;
21
21
  getToken?: () => Promise<string>;
22
22
  onError?: (error: unknown) => void;
23
+ /** Reject a failed registry read so the daemon monitor can distinguish it from an explicit OFF. */
24
+ throwOnError?: boolean;
23
25
  }
24
26
  export declare function readDaemonFlag(flagKey: string, options?: ReadDaemonFlagOptions): Promise<boolean>;
25
27
  //# sourceMappingURL=flag.d.ts.map
@@ -18,8 +18,10 @@ export async function readDaemonFlag(flagKey, options = {}) {
18
18
  if (override !== null)
19
19
  return override;
20
20
  let failed = false;
21
+ let reportedError;
21
22
  const onError = (error) => {
22
23
  failed = true;
24
+ reportedError ??= error;
23
25
  options.onError?.(error);
24
26
  };
25
27
  let client;
@@ -41,10 +43,17 @@ export async function readDaemonFlag(flagKey, options = {}) {
41
43
  });
42
44
  await client.ready();
43
45
  const resolution = client.explain(flagKey, { fallback: false });
46
+ if (failed && options.throwOnError) {
47
+ throw new Error("hq-flags registry read failed", { cause: reportedError });
48
+ }
44
49
  return !failed && resolution.source === "snapshot" && resolution.value === true;
45
50
  }
46
51
  catch (error) {
47
- onError(error);
52
+ if (!failed)
53
+ onError(error);
54
+ if (options.throwOnError && failed) {
55
+ throw new Error("hq-flags registry read failed", { cause: reportedError ?? error });
56
+ }
48
57
  return false;
49
58
  }
50
59
  finally {
@@ -6,7 +6,8 @@
6
6
  * machine. Sessions in any repo ask it which company they belong to and
7
7
  * register themselves here.
8
8
  */
9
- import { type ResolveResult } from "../registry/registry.js";
9
+ import { readRegistry, type ResolveResult } from "../registry/registry.js";
10
+ import { readCompaniesManifestMap } from "../work-context/repo-remote.js";
10
11
  import { SessionRegistry } from "./session-registry.js";
11
12
  import { HqMcpBridge, type RunWorkerRequest } from "./ops/mcp.js";
12
13
  import { type StdinMachineCredentials } from "./machine-login.js";
@@ -26,6 +27,8 @@ export interface HqdServerOptions {
26
27
  workMeshRoot?: string;
27
28
  /** Test seam for the hq-anywhere-runtime default-off gate. */
28
29
  mcpEnabled?: () => boolean | Promise<boolean>;
30
+ /** Test seam for the live hq-flags read used by the daemon's bounded runtime monitor. */
31
+ runtimeFlagReader?: () => Promise<boolean>;
29
32
  /** Test seam for the hosted Streamable HTTP MCP transport. */
30
33
  mcpBridge?: HqMcpBridge;
31
34
  /** Test seam for machine login while keeping production credentials off disk. */
@@ -43,6 +46,11 @@ export interface HqdServer {
43
46
  close(): Promise<void>;
44
47
  }
45
48
  /** Resolve cwd → company through the registry, cached until registry.json changes. */
46
- export declare function createResolver(registryPath: string): (cwd: string) => ResolveResult;
49
+ export declare function createResolver(registryPath: string, options?: {
50
+ hqRoot?: string;
51
+ log?: HqdServerOptions["log"];
52
+ readRegistry?: typeof readRegistry;
53
+ readCompaniesManifest?: typeof readCompaniesManifestMap;
54
+ }): (cwd: string) => ResolveResult;
47
55
  export declare function startHqdServer(opts: HqdServerOptions): Promise<HqdServer>;
48
56
  //# sourceMappingURL=server.d.ts.map
@@ -10,7 +10,8 @@ import * as fs from "node:fs";
10
10
  import * as net from "node:net";
11
11
  import * as path from "node:path";
12
12
  import { CLI_VERSION } from "../../cli-version.js";
13
- import { canonicalDir, readRegistry, registryFile, resolveCompanyForDir } from "../registry/registry.js";
13
+ import { canonicalDir, emptyRegistry, readRegistry, registryFile, resolveCompanyForDir } from "../registry/registry.js";
14
+ import { readCompaniesManifestMap } from "../work-context/repo-remote.js";
14
15
  import { createLineSplitter, encodeMessage, HQD_DEFAULT_REQUEST_TIMEOUT_MS, HQD_MAX_LINE_BYTES, parseRequestLine } from "./protocol.js";
15
16
  import { SessionRegistry } from "./session-registry.js";
16
17
  import { journalAppend, JournalOpError } from "./ops/journal.js";
@@ -20,7 +21,9 @@ import { PolicyIndex, PolicyOpError, policyCheck } from "./ops/policy.js";
20
21
  import { defaultMcpToken, HqMcpBridge, jsonResult, McpOpError, runWorker } from "./ops/mcp.js";
21
22
  import { authenticateMachineCredentials, machineTokenExpiresAt, MachineLoginError, parseStdinMachineCredentials } from "./machine-login.js";
22
23
  import { HQ_MCP_TOOL_NAMES, HQD_NATIVE_TOOL_NAMES } from "../mcp/tools.js";
23
- import { anywhereRuntimeEnabled, HQ_ANYWHERE_RUNTIME_FLAG } from "../../commands/anywhere-runtime-flag.js";
24
+ import { HQ_ANYWHERE_RUNTIME_FLAG } from "../../commands/anywhere-runtime-flag.js";
25
+ import { createBoundedDaemonFlagMonitor } from "./flag-monitor.js";
26
+ import { readDaemonFlag } from "./flag.js";
24
27
  // The allowlist is deliberately read-only. A newly added hosted MCP tool must
25
28
  // pass hq.policy_check until it is explicitly reviewed as read-only here.
26
29
  const MCP_READ_ONLY_TOOL_NAMES = new Set([
@@ -62,29 +65,89 @@ function stringArg(args, name, required = true) {
62
65
  throw new OpError("bad_args", `${name} must be a non-empty string`);
63
66
  }
64
67
  /** Resolve cwd → company through the registry, cached until registry.json changes. */
65
- export function createResolver(registryPath) {
68
+ export function createResolver(registryPath, options = {}) {
69
+ const loadRegistry = options.readRegistry ?? readRegistry;
70
+ const loadCompaniesManifest = options.readCompaniesManifest ?? readCompaniesManifestMap;
66
71
  const cache = new Map();
67
- let stamp = "";
72
+ let registryStamp = "";
73
+ let registry = emptyRegistry();
74
+ let registryLoadFailed = false;
75
+ let manifestStamp = "";
76
+ let companies = null;
77
+ let manifestLoadFailed = false;
68
78
  return (cwd) => {
69
- let current = "missing";
79
+ let currentRegistryStamp = "missing";
70
80
  try {
71
81
  const st = fs.statSync(registryPath);
72
- current = `${st.mtimeMs}:${st.size}:${st.ino}`;
82
+ currentRegistryStamp = `${st.mtimeMs}:${st.ctimeMs}:${st.size}:${st.ino}:${st.mode}`;
73
83
  }
74
84
  catch (err) {
75
- if (err.code !== "ENOENT")
76
- throw err;
85
+ const code = err.code;
86
+ if (code !== "ENOENT") {
87
+ cache.clear();
88
+ const detail = err instanceof Error ? err.message : String(err);
89
+ options.log?.("warn", `hqd registry unavailable; using personal context: ${detail}`);
90
+ return { company: null, reason: "HQ registry is unavailable; using personal context." };
91
+ }
77
92
  }
78
- if (current !== stamp) {
93
+ if (currentRegistryStamp !== registryStamp) {
79
94
  cache.clear();
80
- stamp = current;
95
+ registryStamp = currentRegistryStamp;
96
+ registryLoadFailed = false;
97
+ if (currentRegistryStamp === "missing") {
98
+ registry = emptyRegistry();
99
+ }
100
+ else {
101
+ try {
102
+ registry = loadRegistry(registryPath);
103
+ }
104
+ catch (err) {
105
+ registryLoadFailed = true;
106
+ const detail = err instanceof Error ? err.message : String(err);
107
+ options.log?.("warn", `hqd registry unavailable; using personal context: ${detail}`);
108
+ }
109
+ }
110
+ }
111
+ if (registryLoadFailed) {
112
+ return { company: null, reason: "HQ registry is unavailable; using personal context." };
81
113
  }
82
114
  const key = canonicalDir(cwd);
83
- const hit = cache.get(key);
84
- if (hit)
85
- return hit;
86
- const result = resolveCompanyForDir(readRegistry(registryPath), key);
115
+ const result = cache.get(key) ?? resolveCompanyForDir(registry, key);
87
116
  cache.set(key, result);
117
+ if (result.company && options.hqRoot) {
118
+ const manifestPath = path.join(options.hqRoot, "companies", "manifest.yaml");
119
+ let currentManifestStamp;
120
+ try {
121
+ const st = fs.statSync(manifestPath);
122
+ currentManifestStamp = `${st.mtimeMs}:${st.ctimeMs}:${st.size}:${st.ino}:${st.mode}`;
123
+ }
124
+ catch (err) {
125
+ const code = err.code;
126
+ currentManifestStamp = code === "ENOENT" ? "missing" : `unavailable:${code ?? "unknown"}`;
127
+ }
128
+ if (currentManifestStamp !== manifestStamp) {
129
+ manifestStamp = currentManifestStamp;
130
+ companies = null;
131
+ manifestLoadFailed = false;
132
+ if (currentManifestStamp === "missing" || currentManifestStamp.startsWith("unavailable:")) {
133
+ manifestLoadFailed = true;
134
+ }
135
+ else {
136
+ companies = loadCompaniesManifest(options.hqRoot);
137
+ manifestLoadFailed = companies === null;
138
+ }
139
+ if (manifestLoadFailed) {
140
+ options.log?.("warn", "hqd companies/manifest.yaml unavailable or malformed; using personal context");
141
+ }
142
+ }
143
+ if (manifestLoadFailed || !companies) {
144
+ return { company: null, reason: "HQ company manifest is unavailable; using personal context." };
145
+ }
146
+ if (!Object.hasOwn(companies, result.company)) {
147
+ options.log?.("warn", `hqd registry company ${result.company} is not present in companies/manifest.yaml; using personal context`);
148
+ return { company: null, reason: `Registry company ${result.company} is not present in companies/manifest.yaml; using personal context.` };
149
+ }
150
+ }
88
151
  return result;
89
152
  };
90
153
  }
@@ -106,12 +169,13 @@ export async function startHqdServer(opts) {
106
169
  const isPipe = process.platform === "win32";
107
170
  const startedAtMs = (opts.now ?? (() => new Date()))().getTime();
108
171
  const sessions = new SessionRegistry(opts.now);
109
- const resolve = createResolver(opts.registryPath ?? registryFile());
110
172
  const log = opts.log ?? (() => { });
173
+ const resolve = createResolver(opts.registryPath ?? registryFile(), { hqRoot: opts.hqRoot, log });
111
174
  const clock = opts.now ?? (() => new Date());
112
175
  const env = opts.env ?? process.env;
113
176
  const policyIndex = opts.hqRoot ? new PolicyIndex(opts.hqRoot, log) : null;
114
177
  let ephemeralMachineToken;
178
+ let flagRevocationGeneration = 0;
115
179
  const requireRoot = () => {
116
180
  if (!opts.hqRoot)
117
181
  throw new OpError("no_hq_root", "hqd has no HQ root configured (set hqRoot in daemon config or HQ_ROOT)");
@@ -138,7 +202,6 @@ export async function startHqdServer(opts) {
138
202
  return explicit ?? boundCompany;
139
203
  };
140
204
  let hqMcp = opts.mcpBridge;
141
- const mcpEnabled = opts.mcpEnabled ?? (() => anywhereRuntimeEnabled(env));
142
205
  if (!isPipe) {
143
206
  fs.mkdirSync(path.dirname(socketPath), { recursive: true, mode: 0o700 });
144
207
  if (fs.existsSync(socketPath)) {
@@ -147,6 +210,22 @@ export async function startHqdServer(opts) {
147
210
  fs.unlinkSync(socketPath);
148
211
  }
149
212
  }
213
+ const runtimeFlagMonitor = opts.mcpEnabled ? undefined : createBoundedDaemonFlagMonitor({
214
+ read: opts.runtimeFlagReader ?? (() => readDaemonFlag(HQ_ANYWHERE_RUNTIME_FLAG, {
215
+ env,
216
+ throwOnError: true,
217
+ })),
218
+ onChange: (enabled) => {
219
+ if (!enabled) {
220
+ flagRevocationGeneration += 1;
221
+ ephemeralMachineToken = undefined;
222
+ }
223
+ },
224
+ onReadError: (error) => {
225
+ log("warn", `hqd runtime flag refresh failed (${error instanceof Error ? error.name : "UnknownError"}); retaining the last decision only until its 60-second limit`);
226
+ },
227
+ });
228
+ const mcpEnabled = opts.mcpEnabled ?? (() => runtimeFlagMonitor.enabled());
150
229
  /** Synchronous ops answer in request order; policy.check and mcp.call may return a promise. */
151
230
  const handle = (req) => {
152
231
  const args = req.args ?? {};
@@ -162,7 +241,7 @@ export async function startHqdServer(opts) {
162
241
  case "auth.status":
163
242
  return Promise.resolve(mcpEnabled()).then((enabled) => {
164
243
  if (!enabled)
165
- throw new OpError("anywhere_disabled", `Daemon login is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
244
+ ephemeralMachineToken = undefined;
166
245
  if (ephemeralMachineToken && ephemeralMachineToken.expiresAt <= clock().getTime())
167
246
  ephemeralMachineToken = undefined;
168
247
  return { signedIn: Boolean(ephemeralMachineToken), tokenSource: ephemeralMachineToken ? "stdin-memory" : null };
@@ -171,10 +250,12 @@ export async function startHqdServer(opts) {
171
250
  return Promise.resolve(mcpEnabled()).then(async (enabled) => {
172
251
  if (!enabled)
173
252
  throw new OpError("anywhere_disabled", `Daemon login is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
253
+ const loginGeneration = flagRevocationGeneration;
174
254
  const credentials = parseStdinMachineCredentials(JSON.stringify(args.credentials) ?? "");
175
255
  const controller = new AbortController();
176
256
  const timeoutMs = opts.machineAuthTimeoutMs ?? 25_000;
177
257
  let timer;
258
+ let authenticatedToken;
178
259
  try {
179
260
  const authentication = (opts.machineAuthenticator ?? ((input, signal) => authenticateMachineCredentials(input, { env, signal })))(credentials, controller.signal);
180
261
  const deadline = new Promise((_, reject) => {
@@ -183,11 +264,10 @@ export async function startHqdServer(opts) {
183
264
  reject(new MachineLoginError("auth_timeout", "hqd sign-in timed out; try again"));
184
265
  }, timeoutMs);
185
266
  });
186
- const token = await Promise.race([authentication, deadline]);
187
- const expiresAt = machineTokenExpiresAt(token);
267
+ authenticatedToken = await Promise.race([authentication, deadline]);
268
+ const expiresAt = machineTokenExpiresAt(authenticatedToken);
188
269
  if (expiresAt <= clock().getTime())
189
270
  throw new MachineLoginError("auth_failed", "hqd sign-in failed; the identity provider returned an expired token");
190
- ephemeralMachineToken = { value: token, expiresAt };
191
271
  }
192
272
  catch {
193
273
  throw new MachineLoginError("auth_failed", "hqd sign-in failed; check the machine credentials and try again");
@@ -196,6 +276,10 @@ export async function startHqdServer(opts) {
196
276
  if (timer)
197
277
  clearTimeout(timer);
198
278
  }
279
+ if (loginGeneration !== flagRevocationGeneration || !(await mcpEnabled())) {
280
+ throw new OpError("anywhere_disabled", `Daemon login is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
281
+ }
282
+ ephemeralMachineToken = { value: authenticatedToken, expiresAt: machineTokenExpiresAt(authenticatedToken) };
199
283
  return { signedIn: true, tokenSource: "stdin-memory" };
200
284
  });
201
285
  case "resolve": {
@@ -344,7 +428,7 @@ export async function startHqdServer(opts) {
344
428
  const deadline = requestStartedAt + timeoutMs;
345
429
  const forward = () => Promise.resolve(mcpEnabled()).then((enabled) => {
346
430
  if (!enabled) {
347
- throw new McpOpError("hq_mcp_unavailable", `Hosted hq-mcp forwarding is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
431
+ throw new OpError("anywhere_disabled", `Hosted hq-mcp forwarding is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
348
432
  }
349
433
  const remainingMs = Math.floor(deadline - Date.now());
350
434
  if (remainingMs <= 0) {
@@ -365,9 +449,14 @@ export async function startHqdServer(opts) {
365
449
  });
366
450
  return hqMcp.callTool(name, raw, remainingMs);
367
451
  });
368
- return !MCP_READ_ONLY_TOOL_NAMES.has(name)
369
- ? policyGuardedCall(forward)
370
- : forward();
452
+ return Promise.resolve(mcpEnabled()).then((enabled) => {
453
+ if (!enabled) {
454
+ throw new OpError("anywhere_disabled", `Hosted hq-mcp forwarding is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
455
+ }
456
+ return !MCP_READ_ONLY_TOOL_NAMES.has(name)
457
+ ? policyGuardedCall(forward)
458
+ : forward();
459
+ });
371
460
  }
372
461
  }
373
462
  }
@@ -463,6 +552,7 @@ export async function startHqdServer(opts) {
463
552
  sessions,
464
553
  close: () => (closing ??= new Promise((done) => {
465
554
  policyIndex?.close();
555
+ runtimeFlagMonitor?.close();
466
556
  hqMcp?.close();
467
557
  server.close(() => done());
468
558
  // Answered-but-idle clients would otherwise hold close() open.
@@ -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)));
@@ -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.
@@ -34,6 +34,23 @@ When the flag is enabled, the global hook uses hqd for events outside the HQ
34
34
  root. Inside the root, the project hook routes through hqd when it is running
35
35
  and uses the direct master hook when it is stopped.
36
36
 
37
+ ## Flag revocation
38
+
39
+ While hqd is running, it refreshes `hq-anywhere-runtime` every 30 seconds.
40
+ Successful reads are never trusted for more than 60 seconds. This gives a
41
+ healthy flag service a 30-second revocation window while limiting reads to two
42
+ per minute per daemon; the five-second flag request timeout bounds the work
43
+ when the service is slow. Staleness uses elapsed monotonic time, so wall-clock
44
+ corrections cannot extend an ON decision.
45
+
46
+ When a read fails, hqd keeps the last successful value only until it is 60
47
+ seconds old. After that, it fails closed. An OFF decision refuses new daemon
48
+ login and hosted MCP calls with `anywhere_disabled` and clears any stdin machine
49
+ token held in memory. `auth.status` remains available and reports signed out.
50
+ An authentication still in progress when revocation arrives fails cleanly;
51
+ hosted MCP calls already forwarded are allowed to finish or return their normal
52
+ request error. The daemon process itself does not terminate.
53
+
37
54
  ## Link a repository
38
55
 
39
56
  Run `hq link <company>` from a repository to associate it with a company. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.341.0",
3
+ "version": "5.341.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {