@indigoai-us/hq-cli 5.345.48 → 5.345.49

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.49] — 2026-10-08
6
+ - Persist terminal lane envelopes and story metadata when a final report arrives after the lane has already stopped.
7
+
8
+ - Keep changelog guard coverage valid after release fragments move into CHANGELOG.md.
9
+ - The CLI now updates the pnpm-managed `hq` install that PATH resolves first, so `hq update` converges when another pnpm home shadows the running process.
10
+ - Preserve an explicit unknown state when the lanes flag registry cannot be read. The four message-delivery and session-wake gates now fail open on unknown; all other lanes gates retain their fail-closed behavior.
11
+ - `hq feedback bug` can include a fail-off, hq-flags-gated agent or human reporter mark derived from runtime context.
12
+ - HQ Anywhere remains enabled when its flag registry is unavailable, while the local kill switch still turns it off.
13
+ - Windows native-resolve cleanup accepts taskkill exit 128 only after confirming the target process has exited.
14
+
5
15
  ## [5.345.48] — 2026-10-08
6
16
  - Add default-off multi-company session locks with ordered `hq-session add/remove company` updates and scope-capability enforcement.
7
17
  - Give a sync runner that reports stopping 60 seconds to persist pending pushes before SIGKILL. The daemon shutdown deadline includes that grace; other services remain capped at 15 seconds.
package/README.md CHANGED
@@ -9,7 +9,8 @@ hq --help
9
9
 
10
10
  The global HQ runtime installs hooks for supported agent runtimes. When the
11
11
  `hq-anywhere-runtime` flag is enabled, the hooks use hqd and a per-user folder
12
- registry. The flag is off by default. See [HQ Anywhere runtime](docs/anywhere-runtime.md)
12
+ registry. The flag defaults on when its registry row cannot be read, and an
13
+ explicit false turns it off. See [HQ Anywhere runtime](docs/anywhere-runtime.md)
13
14
  for install and link commands.
14
15
 
15
16
  ## Live meeting transcripts
@@ -3673,6 +3673,10 @@ export declare const COMMAND_CATALOG: readonly [{
3673
3673
  readonly flags: "--company <slug>";
3674
3674
  readonly description: "Company slug to associate with the report";
3675
3675
  readonly hidden: false;
3676
+ }, {
3677
+ readonly flags: "--reporter <kind>";
3678
+ readonly description: "Mark this bug report as agent-filed (only 'agent' is accepted)";
3679
+ readonly hidden: false;
3676
3680
  }, {
3677
3681
  readonly flags: "--screenshot <path>";
3678
3682
  readonly description: "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)";
@@ -3706,6 +3710,10 @@ export declare const COMMAND_CATALOG: readonly [{
3706
3710
  readonly flags: "--company <slug>";
3707
3711
  readonly description: "Company slug to associate with the report";
3708
3712
  readonly hidden: false;
3713
+ }, {
3714
+ readonly flags: "--reporter <kind>";
3715
+ readonly description: "Mark this bug report as agent-filed (only 'agent' is accepted)";
3716
+ readonly hidden: false;
3709
3717
  }, {
3710
3718
  readonly flags: "--screenshot <path>";
3711
3719
  readonly description: "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)";
@@ -4442,6 +4442,11 @@ export const COMMAND_CATALOG = [
4442
4442
  "description": "Company slug to associate with the report",
4443
4443
  "hidden": false
4444
4444
  },
4445
+ {
4446
+ "flags": "--reporter <kind>",
4447
+ "description": "Mark this bug report as agent-filed (only 'agent' is accepted)",
4448
+ "hidden": false
4449
+ },
4445
4450
  {
4446
4451
  "flags": "--screenshot <path>",
4447
4452
  "description": "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)",
@@ -4482,6 +4487,11 @@ export const COMMAND_CATALOG = [
4482
4487
  "description": "Company slug to associate with the report",
4483
4488
  "hidden": false
4484
4489
  },
4490
+ {
4491
+ "flags": "--reporter <kind>",
4492
+ "description": "Mark this bug report as agent-filed (only 'agent' is accepted)",
4493
+ "hidden": false
4494
+ },
4485
4495
  {
4486
4496
  "flags": "--screenshot <path>",
4487
4497
  "description": "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)",
@@ -1,5 +1,5 @@
1
1
  import { resolveFlagGate, resolveProcessFlagGate } from "../lib/flag-registry.js";
2
- import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
2
+ import { HQ_ANYWHERE_RUNTIME_FLAG, hqAnywhereRuntimeLocallyDisabled } from "../lib/anywhere-runtime-flag-key.js";
3
3
  export { HQ_ANYWHERE_RUNTIME_FLAG };
4
4
  export class AnywhereRuntimeDisabledError extends Error {
5
5
  expected = true;
@@ -9,8 +9,10 @@ export class AnywhereRuntimeDisabledError extends Error {
9
9
  }
10
10
  }
11
11
  export function anywhereRuntimeEnabled(env = process.env) {
12
- const fallback = () => false;
13
- const lookup = { fallback: false };
12
+ if (hqAnywhereRuntimeLocallyDisabled(env))
13
+ return false;
14
+ const fallback = () => true;
15
+ const lookup = { fallback: true };
14
16
  return env === process.env
15
17
  ? resolveProcessFlagGate(HQ_ANYWHERE_RUNTIME_FLAG, lookup, fallback)
16
18
  : resolveFlagGate(undefined, HQ_ANYWHERE_RUNTIME_FLAG, lookup, fallback, env);
@@ -4,6 +4,8 @@ import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs
4
4
  import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
5
5
  export declare const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
6
6
  export declare const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
7
+ export declare const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
8
+ export declare const FEEDBACK_REPORTER_KIND_FLAG_FALLBACK = true;
7
9
  export declare const BODY_MAX_BYTES: number;
8
10
  /** Validate fields that must be valid before feedback attachments are uploaded. */
9
11
  export declare function validateFeedbackContent(title: string, body: string): void;
@@ -44,7 +46,15 @@ export interface FeedbackSubmitOptions {
44
46
  diagnostics?: DiagnosticsBlob;
45
47
  /** Precomputed setting so environment lookup happens before attachments upload. */
46
48
  logsEnabled?: boolean;
49
+ /** Optional one-way override: callers can mark human sessions as agent. */
50
+ reporterOverride?: "agent";
47
51
  }
52
+ export interface FeedbackReporterMark {
53
+ reporter_kind: "agent" | "human";
54
+ reporter_source: string;
55
+ }
56
+ /** Resolve a safe reporter classification without returning any env values. */
57
+ export declare function resolveFeedbackReporter(env?: NodeJS.ProcessEnv, isTTY?: boolean, override?: string): FeedbackReporterMark;
48
58
  export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
49
59
  /**
50
60
  * Attach redacted `~/.hq` log tails to `diagnostics` if, and only if, they fit
@@ -10,6 +10,8 @@ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
10
10
  import { cachedLatestVersion } from "../utils/version-check.js";
11
11
  export const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
12
12
  export const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
13
+ export const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
14
+ export const FEEDBACK_REPORTER_KIND_FLAG_FALLBACK = true;
13
15
  export const BODY_MAX_BYTES = 64 * 1024;
14
16
  /** Validate fields that must be valid before feedback attachments are uploaded. */
15
17
  export function validateFeedbackContent(title, body) {
@@ -46,6 +48,32 @@ function validateFeedbackRequestSize(requestBody) {
46
48
  * after attaching, so this reserve only needs to be roughly right.
47
49
  */
48
50
  const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
51
+ const AGENT_RUNTIME_MARKERS = [
52
+ "HQ_LANE_ID",
53
+ "HQ_POLICY_WORKER_DIR",
54
+ "HQ_AGENT_DIR",
55
+ "CODEX_THREAD_ID",
56
+ "CODEX_SESSION_ID",
57
+ "CLAUDE_CODE_SESSION_ID",
58
+ "CLAUDE_SESSION_ID",
59
+ "HQ_SESSION_ID",
60
+ "GROK_SESSION_ID",
61
+ ];
62
+ /** Resolve a safe reporter classification without returning any env values. */
63
+ export function resolveFeedbackReporter(env = process.env, isTTY = process.stdin.isTTY === true, override) {
64
+ for (const marker of AGENT_RUNTIME_MARKERS) {
65
+ if (env[marker]?.trim()) {
66
+ return { reporter_kind: "agent", reporter_source: marker };
67
+ }
68
+ }
69
+ if (override === "agent") {
70
+ return { reporter_kind: "agent", reporter_source: "CLI_REPORTER_OVERRIDE" };
71
+ }
72
+ return {
73
+ reporter_kind: "human",
74
+ reporter_source: isTTY ? "stdin_isTTY" : "no_agent_runtime_marker",
75
+ };
76
+ }
49
77
  function buildFeedbackRequestBody(opts) {
50
78
  const requestBody = {
51
79
  type: opts.type,
@@ -53,6 +81,10 @@ function buildFeedbackRequestBody(opts) {
53
81
  body: opts.body,
54
82
  diagnostics: opts.diagnostics,
55
83
  };
84
+ if (opts.reporterMark) {
85
+ requestBody.reporter_kind = opts.reporterMark.reporter_kind;
86
+ requestBody.reporter_source = opts.reporterMark.reporter_source;
87
+ }
56
88
  if (opts.company) {
57
89
  requestBody.company = opts.company;
58
90
  }
@@ -124,6 +156,9 @@ export async function submitFeedback(opts) {
124
156
  // which invokes the same validator before uploading any attachments.
125
157
  validateFeedbackContent(opts.title, opts.body);
126
158
  const diagnostics = opts.diagnostics ?? collectDiagnostics();
159
+ const reporterMark = opts.type === "bug" && resolveProcessFlagGate(FEEDBACK_REPORTER_KIND_FLAG, { fallback: FEEDBACK_REPORTER_KIND_FLAG_FALLBACK }, () => FEEDBACK_REPORTER_KIND_FLAG_FALLBACK)
160
+ ? resolveFeedbackReporter(process.env, process.stdin.isTTY === true, opts.reporterOverride)
161
+ : undefined;
127
162
  const requestBody = buildFeedbackRequestBody({
128
163
  type: opts.type,
129
164
  title: opts.title,
@@ -132,6 +167,7 @@ export async function submitFeedback(opts) {
132
167
  diagnostics,
133
168
  screenshots: opts.screenshots,
134
169
  logBundle: opts.logBundle,
170
+ reporterMark,
135
171
  });
136
172
  // Direct callers can supply attachment references; ensure the complete base
137
173
  // envelope still fits before adding optional inline logs or making a request.
@@ -172,12 +208,16 @@ function registerSubcommand(feedbackCmd, type) {
172
208
  .requiredOption("--title <text>", "Short title for the report")
173
209
  .requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
174
210
  .option("--company <slug>", "Company slug to associate with the report")
211
+ .option("--reporter <kind>", "Mark this bug report as agent-filed (only 'agent' is accepted)")
175
212
  .option("--screenshot <path>", `Attach a screenshot (repeatable, up to ${MAX_SCREENSHOTS}; .png/.jpg/.jpeg/.webp/.gif)`, (value, prev) => [...prev, value], [])
176
213
  // Commander turns `--no-logs` into a boolean `logs` that defaults to true,
177
214
  // so the default is "attach logs" and the flag is the opt-out.
178
215
  .option("--no-logs", "Do not attach redacted tails of your ~/.hq log files (also: HQ_FEEDBACK_LOGS=0)")
179
216
  .action(async (opts) => {
180
217
  try {
218
+ if (opts.reporter !== undefined && (opts.reporter !== "agent" || type !== "bug")) {
219
+ throw new Error("Invalid --reporter value: only bug reports may use 'agent'.");
220
+ }
181
221
  const token = await ensureCognitoToken({ interactive: false });
182
222
  const body = await readBodyFile(opts.bodyFile);
183
223
  validateFeedbackContent(opts.title, body);
@@ -192,6 +232,9 @@ function registerSubcommand(feedbackCmd, type) {
192
232
  diagnostics.latestVersion = latestVersion;
193
233
  }
194
234
  const logsEnabled = opts.logs !== false && feedbackLogsEnabled();
235
+ const reporterMark = type === "bug" && resolveProcessFlagGate(FEEDBACK_REPORTER_KIND_FLAG, { fallback: FEEDBACK_REPORTER_KIND_FLAG_FALLBACK }, () => FEEDBACK_REPORTER_KIND_FLAG_FALLBACK)
236
+ ? resolveFeedbackReporter(process.env, process.stdin.isTTY === true, opts.reporter)
237
+ : undefined;
195
238
  // Validate the serialized server request envelope before screenshots
196
239
  // or the log bundle can create remote objects. Attachment references
197
240
  // are added and checked again inside submitFeedback after upload.
@@ -201,6 +244,7 @@ function registerSubcommand(feedbackCmd, type) {
201
244
  body,
202
245
  company: opts.company,
203
246
  diagnostics,
247
+ reporterMark,
204
248
  }));
205
249
  // vaultApiFetch can reject an hqk_ token or an invalid API base URL
206
250
  // locally before fetch. Check the mandatory POST, and the screenshot
@@ -236,6 +280,7 @@ function registerSubcommand(feedbackCmd, type) {
236
280
  includeLogs: opts.logs !== false,
237
281
  logBundle,
238
282
  diagnostics,
283
+ reporterOverride: opts.reporter === "agent" ? "agent" : undefined,
239
284
  logsEnabled,
240
285
  });
241
286
  console.log(`Submitted: ${result.id}`);
@@ -13,7 +13,7 @@ import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice
13
13
  import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
14
14
  import { findRetiredLaneEntry, keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
15
15
  import { isTerminalLaneState } from "../lib/lanes/types.js";
16
- import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
16
+ import { readLanesRegistryFlag, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
17
17
  import { appendLaneFocusOverride, focusGuardRefusal, makeFocusOverride } from "../lib/lanes/focus-guard.js";
18
18
  import { appendProjectFocusOverride, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
19
19
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -192,7 +192,7 @@ export function createLaneMessageHandlers(deps) {
192
192
  : "superseded";
193
193
  }
194
194
  if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
195
- const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlag(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
195
+ const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlagFailOpen(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
196
196
  if (runningSupersededMessageDeliveryEnabled) {
197
197
  const latestLane = readLane(ctx.hqRoot(), currentLane.lane_id);
198
198
  if (latestLane.state === "killed" || latestLane.state === "cancelled") {
@@ -36,7 +36,7 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
36
36
  import { laneDepthCap } from "../lib/lanes/depth-cap.js";
37
37
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
38
38
  import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
39
- import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
39
+ import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
40
40
  import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
41
41
  import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
42
42
  import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
@@ -1898,7 +1898,7 @@ async function resolveMessageLaneTarget(ctx, laneId) {
1898
1898
  const localOverride = readLanesFlagLocalOverride(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, ctx.env);
1899
1899
  return localOverride ?? "check-company";
1900
1900
  }
1901
- return readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
1901
+ return readLanesRegistryFlagFailOpen(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
1902
1902
  company,
1903
1903
  env: ctx.env,
1904
1904
  });
@@ -16,12 +16,16 @@ import * as os from "node:os";
16
16
  import * as path from "node:path";
17
17
  import { CLI_VERSION } from "../cli-version.js";
18
18
  import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
19
- import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
20
19
  import { createLineSplitter, HqdClientError, hqdRequest, hqdSocketPath } from "../lib/daemon/protocol.js";
21
20
  import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
22
21
  import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, RESIDENT_PACKAGE_MODULES } from "../utils/resident-package-preload.js";
23
22
  export const MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
24
23
  const UNREACHABLE_CODES = new Set(["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE", "ETIMEDOUT", "ESOCKET", "EACCES"]);
24
+ function hasPersonTokenCache(env, home) {
25
+ // Mirrors personTokenCacheFile() in utils/cognito-session.ts; duplicated so this path does not load that module.
26
+ const stateDir = env.HQ_STATE_DIR?.trim() || path.join(home, ".hq");
27
+ return fs.existsSync(path.join(stateDir, "cognito-tokens.json"));
28
+ }
25
29
  /** HQ root for prompts: HQ_ROOT, else the ~/.hq/root pointer written by `hq install --global`. */
26
30
  export function readHqRoot(env, home) {
27
31
  const fromEnv = env.HQ_ROOT?.trim();
@@ -54,9 +58,18 @@ export async function runMcpServe(opts = {}) {
54
58
  const cwd = opts.cwd ?? process.cwd();
55
59
  const socketPath = hqdSocketPath(home, env);
56
60
  const timeoutMs = opts.timeoutMs ?? 120_000;
57
- if (!(await (opts.runtimeAllowed ?? (() => anywhereRuntimeAllowed(env, {
58
- onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
59
- })))())) {
61
+ const runtimeAllowed = opts.runtimeAllowed ?? (async () => {
62
+ // A person token is required to read the opt-in setting. Deny a fresh
63
+ // install before loading the hq-flags and Cognito readers.
64
+ if (!hasPersonTokenCache(env, home))
65
+ return false;
66
+ const { anywhereRuntimeAllowed } = await import("../lib/anywhere-person-setting.js");
67
+ return anywhereRuntimeAllowed(env, {
68
+ home,
69
+ onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
70
+ });
71
+ });
72
+ if (!(await runtimeAllowed())) {
60
73
  process.stderr.write(`HQ Anywhere is not enabled. The ${HQ_ANYWHERE_RUNTIME_FLAG} flag and your HQ Anywhere person setting must both be on.\n`);
61
74
  process.exitCode = 1;
62
75
  return;
@@ -6,6 +6,7 @@ export interface ReadHqAnywherePersonSettingOptions {
6
6
  fetchImpl?: typeof fetch;
7
7
  apiBaseUrl?: string;
8
8
  env?: NodeJS.ProcessEnv;
9
+ home?: string;
9
10
  onError?: (errorName: string) => void;
10
11
  }
11
12
  /** Returns null when the person preference cannot be read; callers must fail closed. */
@@ -1,8 +1,20 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
1
4
  import { anywhereRuntimeEnabledFromSnapshot } from "./anywhere-runtime-fast-flag.js";
5
+ function hasPersonTokenCache(env, home) {
6
+ // Mirrors personTokenCacheFile() in utils/cognito-session.ts; duplicated so this path does not load that module.
7
+ const stateDir = env.HQ_STATE_DIR?.trim() || path.join(home, ".hq");
8
+ return fs.existsSync(path.join(stateDir, "cognito-tokens.json"));
9
+ }
2
10
  /** Returns null when the person preference cannot be read; callers must fail closed. */
3
11
  export async function readHqAnywherePersonSetting(options = {}) {
4
12
  const env = options.env ?? process.env;
5
13
  try {
14
+ // Avoid loading the Cognito SDK on the common fresh-install path. There is
15
+ // no person token to refresh or use until a cache exists.
16
+ if (!options.getPersonToken && !hasPersonTokenCache(env, options.home ?? env.HOME ?? os.homedir()))
17
+ return null;
6
18
  const getPersonToken = options.getPersonToken ?? (async () => {
7
19
  const { ensureCognitoToken } = await import("../utils/cognito-session.js");
8
20
  return ensureCognitoToken({ interactive: false, tokenSource: "person" });
@@ -1,9 +1,11 @@
1
1
  import { readLocalOverride, resolveFlag } from "@indigoai-us/hq-flags-client";
2
2
  import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
3
3
  import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from "./flag-snapshot-file.js";
4
- import { HQ_ANYWHERE_RUNTIME_FLAG } from "./anywhere-runtime-flag-key.js";
4
+ import { HQ_ANYWHERE_RUNTIME_FLAG, hqAnywhereRuntimeLocallyDisabled } from "./anywhere-runtime-flag-key.js";
5
5
  /** Resolve the Anywhere gate from hq-flags without loading the CLI's large worker bundle. */
6
6
  export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
7
+ if (hqAnywhereRuntimeLocallyDisabled(env))
8
+ return false;
7
9
  const local = readLocalOverride(HQ_ANYWHERE_RUNTIME_FLAG, env);
8
10
  if (local !== null)
9
11
  return local;
@@ -17,13 +19,13 @@ export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
17
19
  const snapshot = persisted
18
20
  ? { version: 0, flags: persisted.flags }
19
21
  : null;
20
- // HQ Anywhere authorizes a capability, so a missing or stale snapshot fails closed.
22
+ // The flag is a fail-safe kill switch: unknown registry state keeps HQ Anywhere enabled.
21
23
  return resolveFlag({
22
24
  flagKey: HQ_ANYWHERE_RUNTIME_FLAG,
23
25
  env,
24
26
  snapshot,
25
27
  companyIdentifiers: [company.companyUid, company.companySlug].filter((value) => Boolean(value)),
26
- lookup: { fallback: false },
28
+ lookup: { fallback: true },
27
29
  }).value;
28
30
  }
29
31
  //# sourceMappingURL=anywhere-runtime-fast-flag.js.map
@@ -1,3 +1,5 @@
1
- /** hq-flags key for the staged HQ Anywhere runtime. The registry default is off. */
1
+ /** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
2
2
  export declare const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ /** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
4
+ export declare function hqAnywhereRuntimeLocallyDisabled(env?: NodeJS.ProcessEnv): boolean;
3
5
  //# sourceMappingURL=anywhere-runtime-flag-key.d.ts.map
@@ -1,3 +1,8 @@
1
- /** hq-flags key for the staged HQ Anywhere runtime. The registry default is off. */
1
+ /** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
2
2
  export const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ /** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
4
+ export function hqAnywhereRuntimeLocallyDisabled(env = process.env) {
5
+ const value = env.HQ_FLAG_HQ_ANYWHERE_RUNTIME;
6
+ return value === "false" || value === "0";
7
+ }
3
8
  //# sourceMappingURL=anywhere-runtime-flag-key.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Feature flags read by the daemon. The daemon runs for weeks, so each read
3
3
  * asks the registry again instead of holding a per-process answer, and it
4
- * reads as off unless the registry itself says on (or a local override does).
4
+ * reads as each flag's fallback unless the registry or a local override decides it.
5
5
  */
6
6
  import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
7
7
  /** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Feature flags read by the daemon. The daemon runs for weeks, so each read
3
3
  * asks the registry again instead of holding a per-process answer, and it
4
- * reads as off unless the registry itself says on (or a local override does).
4
+ * reads as each flag's fallback unless the registry or a local override decides it.
5
5
  */
6
6
  import { createFlagClient, readLocalOverride, } from "@indigoai-us/hq-flags-client";
7
7
  import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
8
8
  import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
9
9
  import { resolveProcessFlagCompanyContext } from "../flag-registry.js";
10
+ import { HQ_ANYWHERE_RUNTIME_FLAG, hqAnywhereRuntimeLocallyDisabled } from "../anywhere-runtime-flag-key.js";
10
11
  /** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
11
12
  export const SUPERVISOR_HANDOFF_FLAG = "daemon.supervisor-handoff";
12
13
  /** Self-updates build the new CLI beside the running one and swap it in, so `hq` never disappears mid-update. */
@@ -14,6 +15,9 @@ export const STAGED_SELF_UPDATE_FLAG = "cli.staged-self-update";
14
15
  const READ_TIMEOUT_MS = 5_000;
15
16
  export async function readDaemonFlag(flagKey, options = {}) {
16
17
  const env = options.env ?? process.env;
18
+ if (flagKey === HQ_ANYWHERE_RUNTIME_FLAG && hqAnywhereRuntimeLocallyDisabled(env))
19
+ return false;
20
+ const fallback = flagKey === HQ_ANYWHERE_RUNTIME_FLAG;
17
21
  const override = readLocalOverride(flagKey, env);
18
22
  const localOverrideMode = options.localOverrideMode ?? "respect";
19
23
  if (override !== null && (localOverrideMode === "respect" || (localOverrideMode === "off-only" && !override)))
@@ -28,10 +32,10 @@ export async function readDaemonFlag(flagKey, options = {}) {
28
32
  let client;
29
33
  try {
30
34
  const company = resolveProcessFlagCompanyContext(env);
31
- // Signed out reads as off without reaching the registry.
35
+ // Signed out reads as the flag fallback without reaching the registry.
32
36
  const token = await (options.getToken ?? (() => ensureCognitoIdToken({ interactive: false })))();
33
37
  if (typeof token !== "string" || token.trim() === "")
34
- return false;
38
+ return fallback;
35
39
  client = (options.createClient ?? createFlagClient)({
36
40
  endpoint: resolveFlagRegistryEndpoint(undefined, env),
37
41
  getToken: () => token,
@@ -42,20 +46,27 @@ export async function readDaemonFlag(flagKey, options = {}) {
42
46
  ...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
43
47
  onError,
44
48
  });
45
- await client.ready();
46
- const resolution = client.explain(flagKey, { fallback: false });
47
- if (failed && options.throwOnError) {
49
+ try {
50
+ await client.ready();
51
+ }
52
+ catch (error) {
53
+ onError(error);
54
+ }
55
+ const resolution = client.explain(flagKey, { fallback });
56
+ if (failed && options.throwOnError && !fallback) {
48
57
  throw new Error("hq-flags registry read failed", { cause: reportedError });
49
58
  }
50
- return !failed && resolution.source === "snapshot" && resolution.value === true;
59
+ if (resolution.source === "snapshot")
60
+ return resolution.value === true;
61
+ return fallback && resolution.value === true;
51
62
  }
52
63
  catch (error) {
53
64
  if (!failed)
54
65
  onError(error);
55
- if (options.throwOnError && failed) {
66
+ if (options.throwOnError && failed && !fallback) {
56
67
  throw new Error("hq-flags registry read failed", { cause: reportedError ?? error });
57
68
  }
58
- return false;
69
+ return fallback;
59
70
  }
60
71
  finally {
61
72
  try {
@@ -39,7 +39,7 @@ export interface HqdServerOptions {
39
39
  env?: NodeJS.ProcessEnv;
40
40
  /** Work-mesh root override for mesh.presence (tests). */
41
41
  workMeshRoot?: string;
42
- /** Test seam for the hq-anywhere-runtime default-off gate. */
42
+ /** Test seam for the hq-anywhere-runtime fail-safe gate. */
43
43
  mcpEnabled?: () => boolean | Promise<boolean>;
44
44
  /** Test seam for the live hq-flags read used by the daemon's bounded runtime monitor. */
45
45
  runtimeFlagReader?: (options: ReadDaemonFlagOptions) => Promise<boolean>;
@@ -22,7 +22,7 @@ import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDa
22
22
  import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, nextEnvelopePath, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
23
23
  import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
24
24
  import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
25
- import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "./registry-flag.js";
25
+ import { readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "./registry-flag.js";
26
26
  import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, LANE_LEAD_ADMISSION_PRIORITY_FLAG, LANE_QUOTA_ACCOUNT_FAILOVER_FLAG, } from "./flag-keys.js";
27
27
  import { checkLaneDiskSpace, configuredCodexRolloutHomes, laneDiskAccount, retainCodexRollouts } from "./disk-guard.js";
28
28
  import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
@@ -1216,6 +1216,7 @@ export async function runPassBody(hqRoot, deps) {
1216
1216
  }
1217
1217
  }
1218
1218
  const readRegistryFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
1219
+ const readRegistryFlagFailOpen = deps.readRegistryFlag ?? readLanesRegistryFlagFailOpen;
1219
1220
  const storyCloseCompanies = new Map();
1220
1221
  for (const lane of lanes) {
1221
1222
  if (lane.depth < 1 || lane.state !== "interrupted" || lane.keep_alive === true ||
@@ -1303,10 +1304,10 @@ export async function runPassBody(hqRoot, deps) {
1303
1304
  readRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
1304
1305
  readRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
1305
1306
  hasBlockedKeepAliveLane
1306
- ? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, options)
1307
+ ? readRegistryFlagFailOpen(LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, options)
1307
1308
  : Promise.resolve(false),
1308
1309
  readRegistryFlag(LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, options),
1309
- readRegistryFlag(LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, options),
1310
+ readRegistryFlagFailOpen(LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, options),
1310
1311
  ]);
1311
1312
  keepAliveFlagByCompany.set(key, keepAliveEnabled);
1312
1313
  blockedResumeFlagByCompany.set(key, blockedResumeEnabled);
@@ -3585,6 +3586,7 @@ async function runTerminalKeepAliveCandidatePass(hqRoot, lanes, summary, deps, n
3585
3586
  if (company === null)
3586
3587
  return summary;
3587
3588
  const readFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
3589
+ const readFlagFailOpen = deps.readRegistryFlag ?? readLanesRegistryFlagFailOpen;
3588
3590
  const options = { company, env: deps.env ?? process.env };
3589
3591
  let keepAliveEnabled;
3590
3592
  let lineageAware;
@@ -3595,7 +3597,7 @@ async function runTerminalKeepAliveCandidatePass(hqRoot, lanes, summary, deps, n
3595
3597
  readFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
3596
3598
  readFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
3597
3599
  readFlag(LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, options),
3598
- readFlag(LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, options),
3600
+ readFlagFailOpen(LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, options),
3599
3601
  ]);
3600
3602
  }
3601
3603
  catch (err) {
@@ -19,12 +19,22 @@ export interface ReadLanesFlagOptions {
19
19
  reportFailure?: (error: unknown) => void;
20
20
  }
21
21
  export declare function readLanesFlagLocalOverride(flagKey: string, env?: NodeJS.ProcessEnv): boolean | null;
22
+ export type LanesRegistryFlagResult = {
23
+ state: "resolved";
24
+ value: boolean;
25
+ } | {
26
+ state: "unknown";
27
+ };
22
28
  /**
23
29
  * Read a lanes flag once for each company identity in this CLI process. The
24
- * cache contains only an in-flight or settled boolean promise; it is never
30
+ * cache contains only an in-flight or settled decision result; it is never
25
31
  * written to disk. A local override is checked on every call and wins first.
26
32
  */
27
33
  export declare function readLanesRegistryFlag(flagKey: string, options?: ReadLanesFlagOptions): Promise<boolean>;
34
+ /** Read a lanes flag while preserving registry failure as an explicit unknown state. */
35
+ export declare function readLanesRegistryFlagResult(flagKey: string, options?: ReadLanesFlagOptions): Promise<LanesRegistryFlagResult>;
36
+ /** Delivery and wake call sites opt in to availability when the registry is unreadable. */
37
+ export declare function readLanesRegistryFlagFailOpen(flagKey: string, options?: ReadLanesFlagOptions): Promise<boolean>;
28
38
  /**
29
39
  * Flag readers normally report their own failures and return the safe default.
30
40
  * Keep enforcement callers safe if an injected or unexpected reader rejects.
@@ -8,6 +8,7 @@ export function readLanesFlagLocalOverride(flagKey, env = process.env) {
8
8
  }
9
9
  const registryReads = new Map();
10
10
  const reportedThrownFlagReads = new Set();
11
+ const reportedRegistryFailureFallbacks = new Set();
11
12
  class MissingFlagTokenError extends Error {
12
13
  constructor() {
13
14
  super("No cached HQ identity token is available");
@@ -17,13 +18,13 @@ class MissingFlagTokenError extends Error {
17
18
  function errorClass(error) {
18
19
  return error instanceof Error ? error.name : "UnknownError";
19
20
  }
20
- function reportReadFailure(flagKey, error) {
21
+ function reportReadFailure(flagKey, error, fallback) {
21
22
  const suffix = flagKey.replace(/^lanes\./, "").replaceAll(/[^a-zA-Z0-9]+/g, "_");
22
23
  process.stderr.write(`${JSON.stringify({
23
24
  event: `lanes.${suffix}.flag_read_failed`,
24
25
  flag: flagKey,
25
26
  error: errorClass(error),
26
- fallback: false,
27
+ fallback,
27
28
  })}\n`);
28
29
  }
29
30
  function reportInvalidCompanyUid(flagKey, source) {
@@ -46,27 +47,42 @@ function cacheKey(flagKey, company, endpoint) {
46
47
  }
47
48
  /**
48
49
  * Read a lanes flag once for each company identity in this CLI process. The
49
- * cache contains only an in-flight or settled boolean promise; it is never
50
+ * cache contains only an in-flight or settled decision result; it is never
50
51
  * written to disk. A local override is checked on every call and wins first.
51
52
  */
52
53
  export async function readLanesRegistryFlag(flagKey, options = {}) {
54
+ const result = await readLanesRegistryFlagResult(flagKey, options);
55
+ return result.state === "resolved" ? result.value : false;
56
+ }
57
+ /** Read a lanes flag while preserving registry failure as an explicit unknown state. */
58
+ export async function readLanesRegistryFlagResult(flagKey, options = {}) {
59
+ const outcome = await readRegistryFlagOutcome(flagKey, options, false);
60
+ return outcome.result;
61
+ }
62
+ async function readRegistryFlagOutcome(flagKey, options, failOpen) {
53
63
  const env = options.env ?? process.env;
54
64
  const localOverride = readLanesFlagLocalOverride(flagKey, env);
55
65
  if (localOverride !== null)
56
- return localOverride;
66
+ return { result: { state: "resolved", value: localOverride } };
57
67
  const companyUid = options.company?.uid?.trim();
58
68
  if (companyUid && !companyUid.startsWith("cmp_")) {
59
69
  reportInvalidCompanyUid(flagKey, options.companyUidSource ?? "company.uid");
60
- return false;
70
+ return { result: { state: "resolved", value: false } };
61
71
  }
62
72
  const endpoint = resolveFlagRegistryEndpoint(undefined, env);
63
73
  const key = cacheKey(flagKey, options.company, endpoint);
64
74
  const cached = registryReads.get(key);
65
- if (cached)
66
- return cached;
67
- const pending = readRegistryValue(flagKey, endpoint, options);
68
- registryReads.set(key, pending);
69
- return pending;
75
+ const pending = cached ?? readRegistryValue(flagKey, endpoint, options);
76
+ if (!cached)
77
+ registryReads.set(key, pending);
78
+ const outcome = await pending;
79
+ reportRegistryFailure(flagKey, key, outcome, options, failOpen);
80
+ return outcome;
81
+ }
82
+ /** Delivery and wake call sites opt in to availability when the registry is unreadable. */
83
+ export async function readLanesRegistryFlagFailOpen(flagKey, options = {}) {
84
+ const outcome = await readRegistryFlagOutcome(flagKey, options, true);
85
+ return outcome.result.state === "unknown" ? true : outcome.result.value;
70
86
  }
71
87
  /**
72
88
  * Flag readers normally report their own failures and return the safe default.
@@ -97,15 +113,10 @@ async function readRegistryValue(flagKey, endpoint, options) {
97
113
  const companyUid = company?.uid?.trim();
98
114
  const companySlug = company?.slug?.trim();
99
115
  let client;
100
- let enabled = false;
101
- let failed = false;
102
- let reported = false;
103
- const reportFailure = (error) => {
104
- failed = true;
105
- if (reported)
106
- return;
107
- reported = true;
108
- (options.reportFailure ?? ((cause) => reportReadFailure(flagKey, cause)))(error);
116
+ let resolution;
117
+ let failure;
118
+ const rememberFailure = (error) => {
119
+ failure ??= { error };
109
120
  };
110
121
  try {
111
122
  const acquireToken = options.getToken ?? (async () => {
@@ -135,30 +146,58 @@ async function readRegistryValue(flagKey, endpoint, options) {
135
146
  requestTimeoutMs: LANES_FLAG_READ_TIMEOUT_MS,
136
147
  ...(companyUid ? { companyUid } : {}),
137
148
  ...(companySlug ? { companyIdentifiers: [companySlug] } : {}),
138
- onError: reportFailure,
149
+ onError: rememberFailure,
139
150
  });
140
- await client.ready();
141
- const resolution = client.explain(flagKey, { fallback: false });
142
- if (failed)
143
- return false;
144
- if (resolution.source === "snapshot" || resolution.source === "local-override") {
145
- enabled = resolution.value;
151
+ try {
152
+ await client.ready();
153
+ }
154
+ catch (error) {
155
+ rememberFailure(error);
156
+ }
157
+ try {
158
+ resolution = client.explain(flagKey, { fallback: false });
159
+ }
160
+ catch (error) {
161
+ rememberFailure(error);
146
162
  }
147
163
  }
148
164
  catch (error) {
149
- reportFailure(error);
165
+ rememberFailure(error);
150
166
  }
151
167
  finally {
152
168
  try {
153
169
  client?.close();
154
170
  }
155
171
  catch (error) {
156
- reportFailure(error);
172
+ rememberFailure(error);
157
173
  }
158
174
  }
159
- return failed ? false : enabled;
175
+ if (resolution?.source === "snapshot" || resolution?.source === "local-override") {
176
+ return { result: { state: "resolved", value: resolution.value }, ...(failure ? { failure } : {}) };
177
+ }
178
+ if (failure)
179
+ return { result: { state: "unknown" }, failure };
180
+ return { result: { state: "resolved", value: resolution?.value ?? false } };
181
+ }
182
+ function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
183
+ if (!outcome.failure)
184
+ return;
185
+ const fallback = outcome.result.state === "resolved"
186
+ ? outcome.result.value
187
+ : failOpen;
188
+ const reportKey = `${key}:${fallback}`;
189
+ if (reportedRegistryFailureFallbacks.has(reportKey))
190
+ return;
191
+ reportedRegistryFailureFallbacks.add(reportKey);
192
+ const reportFailure = options.reportFailure;
193
+ if (reportFailure) {
194
+ reportFailure(outcome.failure.error);
195
+ return;
196
+ }
197
+ reportReadFailure(flagKey, outcome.failure.error, fallback);
160
198
  }
161
199
  export function _resetLanesRegistryFlagCacheForTests() {
162
200
  registryReads.clear();
201
+ reportedRegistryFailureFallbacks.clear();
163
202
  }
164
203
  //# sourceMappingURL=registry-flag.js.map
@@ -398,8 +398,69 @@ function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}
398
398
  current.state === "interrupted" ||
399
399
  current.state === "cancelled" ||
400
400
  current.state === "killed";
401
- if (terminal)
402
- return current;
401
+ if (terminal) {
402
+ // A terminal state may have been recorded before the worker's final
403
+ // report arrived. The report is durable output from work that already
404
+ // ran, so preserve it without reopening the lane or rechecking admission.
405
+ if (opts.envelope === undefined || opts.envelope === null)
406
+ return current;
407
+ const approvalGate = enforceRegistryApprovalGate(current, opts.envelope);
408
+ effectiveEnvelope = approvalGate.envelope;
409
+ effectiveClassification = approvalGate.violation
410
+ ? { state: "blocked", reason: "approval_gate_violated" }
411
+ : terminalClassification;
412
+ const envelope = effectiveEnvelope;
413
+ const storySwitch = applyStorySwitchEnvelope(current, envelope, at, envelopeRef);
414
+ const next = {
415
+ ...current,
416
+ last_envelope: {
417
+ ref: envelopeRef,
418
+ decision: envelope.decision,
419
+ at,
420
+ artifacts: envelope.artifacts,
421
+ ...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
422
+ ...(envelope.next_check_at !== undefined ? { next_check_at: envelope.next_check_at } : {}),
423
+ },
424
+ ...(envelope.decision !== "blocked" ? { keep_alive_blocked_resume_streak: 0 } : {}),
425
+ envelope_validation_errors: opts.envelopeValidationErrors,
426
+ };
427
+ if (!approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
428
+ next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
429
+ event: "consumed",
430
+ approval_id: approvalId,
431
+ envelope_ref: envelopeRef,
432
+ at,
433
+ }), current.registry_worker_approval_history ?? []);
434
+ }
435
+ if (current.state === "cancelled" || current.state === "killed") {
436
+ return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
437
+ }
438
+ const storyStatus = storySwitch.suppressCompletion
439
+ ? "in_progress"
440
+ : terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
441
+ if (storyStatus !== undefined) {
442
+ next.story_status = storySwitch.suppressCompletion ? "in_progress" : envelope.story_status;
443
+ next.story_complete = storySwitch.suppressCompletion ? false : envelope.story_complete;
444
+ next.story_complete_reason = storySwitch.suppressCompletion ? undefined : envelope.story_complete_reason;
445
+ next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
446
+ ? {
447
+ ...story,
448
+ status: storyStatus,
449
+ updated_at: at,
450
+ ...(envelope.decision === "blocked"
451
+ ? { blocked_reason: envelope.blocked_reason ?? "" }
452
+ : { blocked_reason: undefined }),
453
+ }
454
+ : story);
455
+ }
456
+ if (storySwitch.clearPending)
457
+ next.story_switch_pending = undefined;
458
+ if (storySwitch.storySwitchUnacknowledged !== undefined) {
459
+ next.story_switch_unacknowledged = storySwitch.storySwitchUnacknowledged;
460
+ next.attribution_history = storySwitch.attributionHistory ?? current.attribution_history;
461
+ }
462
+ return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
463
+ }
403
464
  if (current.state === "awaiting_input" || current.state === "blocked") {
404
465
  if (current.state === effectiveClassification.state)
405
466
  return current;
@@ -40,7 +40,7 @@ export const RESIDENT_PACKAGE_MODULES = {
40
40
  "@indigoai-us/hq-cloud",
41
41
  ],
42
42
  "bot-logs-follow": [],
43
- mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
43
+ mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../lib/anywhere-person-setting.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
44
44
  childWrapper: ["../main.js"],
45
45
  coreJobRun: ["../lib/flag-registry.js"],
46
46
  "daemon-call": ["../lib/daemon/protocol.js"],
@@ -76,6 +76,7 @@ export const RESIDENT_PACKAGE_MODULES = {
76
76
  "../utils/plan-gate-error.js",
77
77
  "open",
78
78
  "../commands/mcp-serve.js",
79
+ "../lib/anywhere-person-setting.js",
79
80
  "../lib/daemon/mcp-oauth.js",
80
81
  "../lib/mcp/tools.js",
81
82
  "../lib/mcp/prompts.js",
@@ -187,6 +188,7 @@ const LOADERS = {
187
188
  mcp: [
188
189
  () => import("../lib/mcp/tools.js"),
189
190
  () => import("../lib/mcp/prompts.js"),
191
+ () => import("../lib/anywhere-person-setting.js"),
190
192
  () => import("../utils/cognito-session.js"),
191
193
  () => import("@indigoai-us/hq-cloud"),
192
194
  async () => {
@@ -347,6 +349,7 @@ const LOADERS = {
347
349
  () => import("../utils/plan-gate-error.js"),
348
350
  () => import("open"),
349
351
  () => import("../commands/mcp-serve.js"),
352
+ () => import("../lib/anywhere-person-setting.js"),
350
353
  () => import("../lib/daemon/mcp-oauth.js"),
351
354
  () => import("../lib/mcp/tools.js"),
352
355
  () => import("../lib/mcp/prompts.js"),
@@ -65,7 +65,7 @@ import chalk from "chalk";
65
65
  import { Sentry } from "../sentry.js";
66
66
  import { isCliTelemetryDisabled } from "./cli-telemetry.js";
67
67
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
68
- import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, pnpmPathConflict, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
68
+ import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, resolvePnpmPathInstall, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
69
69
  import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
70
70
  import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
71
71
  import { markLatestIneffective } from "./version-check.js";
@@ -926,17 +926,18 @@ async function attemptUpdateAndReexec(argv, flavor, known, deps, env) {
926
926
  // actually running would stay exactly as stale as it started while the CLI
927
927
  // reported success and re-exec'd into the same build. That is the shape of
928
928
  // the 2026-09-02 gate loop — see isLocalDependencyInstall in version-gate.ts.
929
- const install = (deps.resolveInstall ?? resolveRunningInstall)();
929
+ let install = (deps.resolveInstall ?? resolveRunningInstall)();
930
930
  if (isLocalDependencyInstall(install)) {
931
931
  console.error(chalk.dim(`hq-cli ${latest} is available, but this copy is a local dependency (${install.packageRoot}) — update the project that owns it.`));
932
932
  return { action: "skipped", latest };
933
933
  }
934
934
  if (install.manager === "pnpm") {
935
- const conflict = pnpmPathConflict(install);
936
- if (conflict) {
937
- console.error(chalk.yellow(`⚠ hq self-update refused: ${conflict}`));
935
+ const pathTarget = resolvePnpmPathInstall(install);
936
+ if (pathTarget.refusal) {
937
+ console.error(chalk.yellow(`⚠ hq self-update refused: ${pathTarget.refusal}`));
938
938
  return { action: "skipped", latest };
939
939
  }
940
+ install = pathTarget.install;
940
941
  }
941
942
  const releaseLock = flavor.lock ? (deps.acquireLock ?? acquireUpdateLock)() : () => { };
942
943
  if (!releaseLock)
@@ -194,6 +194,8 @@ export declare function isPrefixWritable(prefix: string, access?: (target: strin
194
194
  * existing ancestor: npm can create a child only if that ancestor is writable.
195
195
  */
196
196
  export declare function canWriteNpmInstall(prefix: string, packageRoot: string, access?: (target: string, mode: number) => void, platform?: NodeJS.Platform): boolean;
197
+ /** Check whether pnpm can replace its global package and PATH shim. */
198
+ export declare function canWritePnpmInstall(install: RunningInstall, access?: (target: string, mode: number) => void): boolean;
197
199
  /** Claim the one local warning/report for this installed version and prefix. */
198
200
  export declare function claimNonWritableNpmPrefixNotice(install: Pick<RunningInstall, "manager" | "prefix" | "packageRoot">, fromVersion: string, homeDir?: string, kind?: "user" | "report"): boolean;
199
201
  /** Emit one path-free warning event for a handled non-writable npm prefix. */
@@ -223,10 +225,12 @@ export declare function derivePnpmHome(install: RunningInstall): string | null;
223
225
  * Environment for the pnpm self-update spawn. pnpm requires its configured
224
226
  * global bin directory to be on PATH; pnpm 12.5.1 reports
225
227
  * `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
226
- * may also lack PNPM_HOME, so derive it from the running install when needed.
228
+ * may also lack PNPM_HOME, so derive it from the selected install when needed.
227
229
  *
228
- * A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
229
- * `undefined` only when the inherited environment already has both values.
230
+ * A no-op for npm/bun installs. The selected install's home owns its global
231
+ * bin directory, so it takes precedence over an inherited PNPM_HOME after the
232
+ * updater retargets the install. Returns `undefined` only when the inherited
233
+ * environment already has the required values.
230
234
  */
231
235
  export declare function pnpmUpdateEnv(install: RunningInstall, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined;
232
236
  export declare function buildPrefixedInstallArgv(prefix: string): string[];
@@ -436,7 +440,10 @@ declare function probeCliVersion(bin: string): string | null;
436
440
  * shims, so writing one home cannot change a different home that appears first
437
441
  * on PATH.
438
442
  */
439
- export declare function pnpmPathConflict(install: RunningInstall): string | null;
443
+ export declare function resolvePnpmPathInstall(install: RunningInstall, canWrite?: (target: RunningInstall) => boolean): {
444
+ install: RunningInstall;
445
+ refusal: string | null;
446
+ };
440
447
  /**
441
448
  * Read-your-writes convergence check, run after an install reports success.
442
449
  *
@@ -101,11 +101,11 @@ function findRunningPackageRoot() {
101
101
  return findCliPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
102
102
  }
103
103
  /** Resolve an on-PATH executable back to the installed hq-cli package it loads. */
104
- function findPackageRootForExecutable(bin) {
104
+ function resolvePackageRootForExecutable(bin) {
105
105
  try {
106
106
  const packageRoot = findCliPackageRoot(path.dirname(realpathSync(bin)));
107
107
  if (packageRoot)
108
- return packageRoot;
108
+ return { packageRoot, candidates: [packageRoot] };
109
109
  }
110
110
  catch {
111
111
  // A missing or inaccessible executable may still be recoverable from the
@@ -115,25 +115,26 @@ function findPackageRootForExecutable(bin) {
115
115
  // resolves symlinks, but a generated shim is a regular file, so walking up
116
116
  // from its real path never reaches the package it invokes. Look up only the
117
117
  // hq-cli package under the matching PNPM_HOME/global layout instead.
118
- const binDir = path.dirname(bin);
118
+ const shimBinDir = path.dirname(bin);
119
119
  const configuredHome = process.env.PNPM_HOME;
120
- const inferredHome = path.basename(binDir).toLowerCase() === "bin"
121
- ? path.dirname(binDir)
122
- : binDir;
120
+ const inferredHome = path.basename(shimBinDir).toLowerCase() === "bin"
121
+ ? path.dirname(shimBinDir)
122
+ : shimBinDir;
123
123
  const candidateHomes = [configuredHome, inferredHome].filter((home, index, homes) => Boolean(home) && homes.findIndex((candidate) => candidate === home) === index);
124
- const home = candidateHomes.find((candidate) => samePath(binDir, candidate) ||
125
- samePath(binDir, path.join(candidate, "bin")));
124
+ const home = candidateHomes.find((candidate) => samePath(shimBinDir, candidate) ||
125
+ samePath(shimBinDir, path.join(candidate, "bin")));
126
126
  if (!home)
127
- return null;
127
+ return { packageRoot: null, candidates: [] };
128
128
  const globalRoot = path.join(home, "global");
129
129
  let storeVersions;
130
130
  try {
131
131
  storeVersions = readdirSync(globalRoot);
132
132
  }
133
133
  catch {
134
- return null;
134
+ return { packageRoot: null, candidates: [] };
135
135
  }
136
136
  const packageSegments = CLI_NAME.split("/");
137
+ const candidates = new Set();
137
138
  for (const storeVersion of storeVersions) {
138
139
  const storeRoot = path.join(globalRoot, storeVersion);
139
140
  const packageRoots = [
@@ -156,11 +157,38 @@ function findPackageRootForExecutable(bin) {
156
157
  if (packageRoot &&
157
158
  samePath(packageRoot, candidate) &&
158
159
  isPnpmManagedPackageDir(packageRoot)) {
159
- return packageRoot;
160
+ candidates.add(packageRoot);
160
161
  }
161
162
  }
162
163
  }
163
- return null;
164
+ const roots = [...candidates];
165
+ if (roots.length <= 1)
166
+ return { packageRoot: roots[0] ?? null, candidates: roots };
167
+ // pnpm's generated shim identifies the package entry point it will execute.
168
+ // Use that target to select among v11 hash directories instead of letting
169
+ // directory enumeration order choose the global project and virtual store.
170
+ let shimContents;
171
+ try {
172
+ shimContents = readFileSync(bin, "utf8").replace(/\\/g, "/");
173
+ }
174
+ catch {
175
+ return { packageRoot: null, candidates: roots };
176
+ }
177
+ const selected = roots.filter((root) => {
178
+ const entryPoint = installedHqEntryPoint({ manager: "pnpm", prefix: null, packageRoot: root });
179
+ if (!entryPoint)
180
+ return false;
181
+ const absolute = entryPoint.replace(/\\/g, "/");
182
+ const relative = path.relative(shimBinDir, entryPoint).replace(/\\/g, "/");
183
+ return shimContents.includes(absolute) || shimContents.includes(relative);
184
+ });
185
+ return {
186
+ packageRoot: selected.length === 1 ? selected[0] : null,
187
+ candidates: roots,
188
+ };
189
+ }
190
+ function findPackageRootForExecutable(bin) {
191
+ return resolvePackageRootForExecutable(bin).packageRoot;
164
192
  }
165
193
  function samePath(left, right) {
166
194
  const canonical = (value) => {
@@ -507,6 +535,39 @@ export function canWriteNpmInstall(prefix, packageRoot, access = (target, mode)
507
535
  }
508
536
  });
509
537
  }
538
+ /** Check whether pnpm can replace its global package and PATH shim. */
539
+ export function canWritePnpmInstall(install, access = (target, mode) => accessSync(target, mode)) {
540
+ const home = derivePnpmHome(install);
541
+ if (!home || !install.packageRoot)
542
+ return false;
543
+ const mode = fsConstants.W_OK | fsConstants.X_OK;
544
+ const dirs = pnpmInstallDirs(install);
545
+ const targets = [
546
+ home,
547
+ path.join(home, "global"),
548
+ path.join(home, "store"),
549
+ ...(dirs ? [dirs.globalDir, dirs.storeDir, dirs.virtualStoreDir] : []),
550
+ path.dirname(install.packageRoot),
551
+ ];
552
+ return targets.every((target) => {
553
+ let candidate = path.resolve(target);
554
+ while (true) {
555
+ try {
556
+ access(candidate, mode);
557
+ return true;
558
+ }
559
+ catch (error) {
560
+ if (error?.code !== "ENOENT")
561
+ return false;
562
+ // pnpm creates missing targets under the nearest existing writable ancestor.
563
+ const parent = path.dirname(candidate);
564
+ if (parent === candidate)
565
+ return false;
566
+ candidate = parent;
567
+ }
568
+ }
569
+ });
570
+ }
510
571
  const nonWritablePrefixNotices = new Set();
511
572
  /** Claim the one local warning/report for this installed version and prefix. */
512
573
  export function claimNonWritableNpmPrefixNotice(install, fromVersion, homeDir = os.homedir(), kind = "user") {
@@ -601,18 +662,22 @@ export function derivePnpmHome(install) {
601
662
  * Environment for the pnpm self-update spawn. pnpm requires its configured
602
663
  * global bin directory to be on PATH; pnpm 12.5.1 reports
603
664
  * `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
604
- * may also lack PNPM_HOME, so derive it from the running install when needed.
665
+ * may also lack PNPM_HOME, so derive it from the selected install when needed.
605
666
  *
606
- * A no-op for npm/bun installs. Never override a caller's PNPM_HOME. Returns
607
- * `undefined` only when the inherited environment already has both values.
667
+ * A no-op for npm/bun installs. The selected install's home owns its global
668
+ * bin directory, so it takes precedence over an inherited PNPM_HOME after the
669
+ * updater retargets the install. Returns `undefined` only when the inherited
670
+ * environment already has the required values.
608
671
  */
609
672
  export function pnpmUpdateEnv(install, base = process.env) {
610
673
  if (install.manager !== "pnpm")
611
674
  return undefined;
612
- const home = base.PNPM_HOME || derivePnpmHome(install);
675
+ const home = derivePnpmHome(install) ?? base.PNPM_HOME;
613
676
  if (!home)
614
677
  return undefined;
615
- const binDir = path.join(home, "bin");
678
+ // pnpm's own `bin -g` command reports PNPM_HOME itself as the global bin
679
+ // directory; it must be that directory, not a guessed `<home>/bin` child.
680
+ const binDir = home;
616
681
  const pathKey = Object.keys(base).find((key) => key.toLowerCase() === "path") ?? "PATH";
617
682
  const currentPath = base[pathKey] ?? "";
618
683
  const normalizedBinDir = path.resolve(binDir);
@@ -633,7 +698,7 @@ export function pnpmUpdateEnv(install, base = process.env) {
633
698
  COREPACK_DEFAULT_TO_LATEST: "0",
634
699
  COREPACK_ENABLE_AUTO_PIN: "0",
635
700
  };
636
- if (!base.PNPM_HOME)
701
+ if (base.PNPM_HOME !== home)
637
702
  result.PNPM_HOME = home;
638
703
  if (!alreadyOnPath)
639
704
  result[pathKey] = [currentPath, binDir].filter(Boolean).join(path.delimiter);
@@ -1401,36 +1466,62 @@ function probeCliVersion(bin) {
1401
1466
  * shims, so writing one home cannot change a different home that appears first
1402
1467
  * on PATH.
1403
1468
  */
1404
- export function pnpmPathConflict(install) {
1469
+ export function resolvePnpmPathInstall(install, canWrite = canWritePnpmInstall) {
1405
1470
  if (install.manager !== "pnpm" || !install.packageRoot)
1406
- return null;
1471
+ return { install, refusal: null };
1407
1472
  const bin = resolveHqOnPath();
1408
1473
  if (!bin)
1409
- return null;
1410
- const pathPackageRoot = findPackageRootForExecutable(bin);
1411
- if (!pathPackageRoot ||
1412
- globalInstallManagerForPath(bin, pathPackageRoot) !== "pnpm") {
1413
- return null;
1414
- }
1415
- // The fallback resolver scans every hash in a PNPM_HOME because generated
1416
- // shims do not expose their target. Multiple installs in the SAME home can
1417
- // therefore produce an arbitrary package root; only a different home means
1418
- // that updating this install cannot change which global shim wins PATH.
1474
+ return { install, refusal: null };
1419
1475
  const runningHome = derivePnpmHome(install);
1476
+ const packageRootLookup = resolvePackageRootForExecutable(bin);
1477
+ const pathPackageRoot = packageRootLookup.packageRoot;
1478
+ if (!pathPackageRoot) {
1479
+ if (packageRootLookup.candidates.length > 1) {
1480
+ return {
1481
+ install,
1482
+ refusal: `could not identify which pnpm package PATH binary ${bin} resolves to; matching global packages: ${packageRootLookup.candidates.join(", ")}`,
1483
+ };
1484
+ }
1485
+ const binDirectory = path.dirname(bin);
1486
+ const binHome = path.basename(binDirectory).toLowerCase() === "bin"
1487
+ ? path.dirname(binDirectory)
1488
+ : binDirectory;
1489
+ if (runningHome && binHome && samePath(runningHome, binHome)) {
1490
+ if (canWrite(install))
1491
+ return { install, refusal: null };
1492
+ return {
1493
+ install,
1494
+ refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
1495
+ };
1496
+ }
1497
+ return { install, refusal: `could not identify the pnpm-managed install resolved by PATH at ${bin}` };
1498
+ }
1499
+ const pathManager = globalInstallManagerForPath(bin, pathPackageRoot);
1500
+ if (pathManager !== "pnpm") {
1501
+ if (pathManager === "npm" || pathManager === "bun") {
1502
+ // Updating the running pnpm install still makes progress when a known
1503
+ // npm/Bun install shadows it; refusing would guarantee no convergence.
1504
+ return { install, refusal: null };
1505
+ }
1506
+ return { install, refusal: `PATH resolves ${bin} to an install that is not verified as pnpm-managed` };
1507
+ }
1508
+ // The resolver uses the generated shim's target when a pnpm v11 home has
1509
+ // multiple package hashes. Its selected package root must drive the global
1510
+ // directory and virtual store used by the update.
1420
1511
  const pathHome = derivePnpmHome({ manager: "pnpm", prefix: null, packageRoot: pathPackageRoot });
1421
- if (!runningHome || !pathHome || samePath(runningHome, pathHome))
1422
- return null;
1423
- let resolvedBin = bin;
1424
- try {
1425
- resolvedBin = realpathSync(bin);
1512
+ if (!runningHome || !pathHome) {
1513
+ return { install, refusal: `could not identify the PNPM_HOME for PATH binary ${bin}` };
1426
1514
  }
1427
- catch {
1428
- // Keep the PATH entry when a platform shim cannot be canonicalized.
1515
+ const targetInstall = samePath(runningHome, pathHome)
1516
+ ? install
1517
+ : { manager: "pnpm", prefix: null, packageRoot: pathPackageRoot };
1518
+ if (!canWrite(targetInstall)) {
1519
+ return {
1520
+ install,
1521
+ refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
1522
+ };
1429
1523
  }
1430
- const version = probeCliVersion(bin) ?? "unknown";
1431
- return (`PATH resolves ${resolvedBin} at version ${version} (manager: pnpm), ` +
1432
- `but this process is running from a different pnpm install at ${install.packageRoot}. ` +
1433
- "Refusing to update one pnpm home while another hq-cli install wins PATH resolution.");
1524
+ return { install: targetInstall, refusal: null };
1434
1525
  }
1435
1526
  /**
1436
1527
  * Read-your-writes convergence check, run after an install reports success.
@@ -5,7 +5,9 @@ local hqd daemon on macOS, Linux, and Windows. On Windows, hqd uses a bundled
5
5
  PowerShell pipe host that applies and verifies a user-only named-pipe DACL
6
6
  before becoming ready. A per-user
7
7
  registry links repositories to HQ companies. The `hq-anywhere-runtime` flag
8
- is the operator kill switch and is off by default. Runtime access also
8
+ is an operator kill switch that defaults on when its registry row is absent,
9
+ archived, or unreadable. An explicit registry false or the local
10
+ `HQ_FLAG_HQ_ANYWHERE_RUNTIME=false|0` override turns it off. Runtime access also
9
11
  requires the current person to enable the `hq-anywhere` setting in HQ. If that
10
12
  setting cannot be read, runtime access stays off.
11
13
 
@@ -34,8 +36,8 @@ stored in a repository or synced HQ folder.
34
36
  Windows requires Windows PowerShell 5.1 or PowerShell 7 for the bundled pipe
35
37
  host. If it is unavailable, the pipe cannot be created with a verified
36
38
  current-user-only DACL, or another process already owns the pipe name, hqd
37
- refuses to start. The `hq-anywhere-runtime` flag remains off by default on all
38
- platforms.
39
+ refuses to start. The flag's missing-row fallback is on across platforms; an
40
+ explicit false remains the operator kill switch.
39
41
 
40
42
  On macOS and Linux, hqd checks the socket directory and socket path with
41
43
  `lstat` before serving. It refuses symlinked, shared, or foreign-owned paths.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.48",
3
+ "version": "5.345.49",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {