@indigoai-us/hq-cli 5.345.48 → 5.345.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -1
  3. package/dist/command-catalog.generated.d.ts +64 -11
  4. package/dist/command-catalog.generated.js +77 -11
  5. package/dist/commands/anywhere-runtime-flag.js +5 -3
  6. package/dist/commands/db-destroy.js +13 -11
  7. package/dist/commands/db-dump.js +3 -3
  8. package/dist/commands/db-remote-deps.d.ts +3 -0
  9. package/dist/commands/db-remote-deps.js +13 -0
  10. package/dist/commands/db-sql.js +4 -3
  11. package/dist/commands/db-usage.js +4 -3
  12. package/dist/commands/dm-forward-flag.d.ts +4 -0
  13. package/dist/commands/dm-forward-flag.js +7 -0
  14. package/dist/commands/dm.d.ts +8 -0
  15. package/dist/commands/dm.js +219 -27
  16. package/dist/commands/feedback.d.ts +10 -0
  17. package/dist/commands/feedback.js +45 -0
  18. package/dist/commands/lanes-message.js +2 -2
  19. package/dist/commands/lanes.js +2 -2
  20. package/dist/commands/mcp-serve.js +17 -4
  21. package/dist/lib/anywhere-person-setting.d.ts +1 -0
  22. package/dist/lib/anywhere-person-setting.js +12 -0
  23. package/dist/lib/anywhere-runtime-fast-flag.js +5 -3
  24. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -1
  25. package/dist/lib/anywhere-runtime-flag-key.js +6 -1
  26. package/dist/lib/daemon/flag.d.ts +1 -1
  27. package/dist/lib/daemon/flag.js +20 -9
  28. package/dist/lib/daemon/hq-cloud.d.ts +2 -2
  29. package/dist/lib/daemon/hq-cloud.js +38 -9
  30. package/dist/lib/daemon/run.js +45 -19
  31. package/dist/lib/daemon/server.d.ts +1 -1
  32. package/dist/lib/db/app-ref.d.ts +21 -0
  33. package/dist/lib/db/app-ref.js +45 -0
  34. package/dist/lib/lanes/keep-alive-audit.d.ts +5 -1
  35. package/dist/lib/lanes/keep-alive-audit.js +85 -3
  36. package/dist/lib/lanes/reconcile.js +6 -4
  37. package/dist/lib/lanes/registry-flag.d.ts +11 -1
  38. package/dist/lib/lanes/registry-flag.js +68 -29
  39. package/dist/lib/lanes/watcher.js +63 -2
  40. package/dist/utils/resident-package-preload.js +4 -1
  41. package/dist/utils/self-update.js +6 -5
  42. package/dist/utils/version-gate.d.ts +11 -4
  43. package/dist/utils/version-gate.js +132 -41
  44. package/docs/anywhere-runtime.md +5 -3
  45. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
5
5
  import { isMachineIdentity } from "@indigoai-us/hq-cloud";
6
6
  import { peekIdToken } from "../utils/id-token.js";
7
7
  import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
8
+ import { dmForwardCliEnabled } from "./dm-forward-flag.js";
8
9
  /**
9
10
  * Classify a recipient arg as an email or a person/agent uid. Mirrors the
10
11
  * email/uid heuristic used by `hq members`. Returns null for neither.
@@ -346,6 +347,32 @@ function friendlyDmError(status, code, fallback) {
346
347
  return `Server error: ${fallback}`;
347
348
  return fallback;
348
349
  }
350
+ function friendlyForwardError(code) {
351
+ switch (code) {
352
+ case "FORWARD_SOURCE_NOT_FOUND":
353
+ return "Source message not found or you cannot read it.";
354
+ case "INVALID_FORWARD_OF":
355
+ return "Invalid source message reference; provide a valid eventId and --from value.";
356
+ case "INVALID_FILE_ACCESS":
357
+ return "Invalid file access choice; use --grant-files or --omit-files.";
358
+ case "FORWARD_FILE_ACCESS_REQUIRED":
359
+ return "Some forwarded files are not readable by the recipient.";
360
+ case "FORWARD_FILES_NOT_ALLOWED":
361
+ return "Files cannot be granted for this channel or cross-company forward.";
362
+ case "CROSS_COMPANY_ACK_REQUIRED":
363
+ return "Warning: this forward crosses company boundaries; review the destination and retry with --cross-company.";
364
+ case "CROSS_COMPANY_FORBIDDEN":
365
+ return "Cross-company forwarding requires owner or admin access in both companies.";
366
+ case "FORWARD_FILE_SHARE_FORBIDDEN":
367
+ return "You do not have permission to share one or more forwarded files.";
368
+ case "FORWARD_FILE_GRANT_FAILED":
369
+ return "The recipient could not be granted access to a forwarded file; no message was sent.";
370
+ case "RECIPIENT_NOT_FOUND":
371
+ return "Recipient not found or not reachable.";
372
+ default:
373
+ return "Message forwarding failed; check the source and destination, then try again.";
374
+ }
375
+ }
349
376
  /**
350
377
  * Resolve a connection request from a pending-requests list by a caller-supplied
351
378
  * identifier (email, personUid, or pairKey). Pure → unit-testable.
@@ -529,6 +556,21 @@ export async function fetchChannelMembers(token, channelId) {
529
556
  const data = (await res.json());
530
557
  return data.members ?? [];
531
558
  }
559
+ /** Resolve structured mentions in a channel message body, failing before post on invalid names. */
560
+ async function resolveChannelBodyMentions(token, channelId, body) {
561
+ const tokens = parseMentionTokens(body);
562
+ if (tokens.length === 0)
563
+ return { ok: true, body, mentions: [] };
564
+ const members = await fetchChannelMembers(token, channelId);
565
+ const resolved = resolveMentions(tokens, members);
566
+ if (!resolved.ok)
567
+ return resolved;
568
+ return {
569
+ ok: true,
570
+ body: stripMentionQuotes(body),
571
+ mentions: resolved.mentions,
572
+ };
573
+ }
532
574
  /** Fetch a channel history page (GET /v1/notify/channels/{id}/messages). Newest-first. */
533
575
  async function fetchChannelMessages(token, channelId, query = {}) {
534
576
  const res = await vaultApiFetch({
@@ -546,9 +588,9 @@ async function fetchChannelMessages(token, channelId, query = {}) {
546
588
  /**
547
589
  * Channel DM path: `hq dm vyg-dev "msg"`, `hq dm '#vyg-dev' "msg"`, or
548
590
  * `hq dm --channel vyg-dev "msg"`. Resolves the caller's channel by name via
549
- * GET /v1/notify/channels, then posts the message into it. Scheduling /
550
- * prompt / details are 1:1-DM features and are rejected here rather than
551
- * silently dropped.
591
+ * GET /v1/notify/channels, then posts the message into it. Scheduling remains
592
+ * a 1:1-DM feature; prompt and details are supported for channel sends while
593
+ * the dm.forward-cli kill switch is enabled.
552
594
  */
553
595
  async function runChannelSend(channelName, message, opts) {
554
596
  try {
@@ -557,12 +599,17 @@ async function runChannelSend(channelName, message, opts) {
557
599
  console.error(chalk.red(`A message body is required: hq dm ${channelName} "<message>" (or hq dm --channel ${channelName} "<message>").`));
558
600
  process.exit(1);
559
601
  }
560
- const unsupported = [
561
- opts.prompt || opts.promptFile ? "--prompt/--prompt-file" : null,
562
- opts.details || opts.detailsFile ? "--details/--details-file" : null,
563
- opts.at ? "--at" : null,
564
- opts.in ? "--in" : null,
565
- ].filter(Boolean);
602
+ const hasChannelContext = opts.prompt !== undefined || opts.promptFile !== undefined ||
603
+ opts.details !== undefined || opts.detailsFile !== undefined;
604
+ if (hasChannelContext && !dmForwardCliEnabled()) {
605
+ console.error(chalk.red("Channel --details/--prompt support is disabled by the dm.forward-cli kill switch."));
606
+ process.exit(1);
607
+ }
608
+ if (opts.promptFile === "" || opts.detailsFile === "") {
609
+ console.error(chalk.red("--prompt-file and --details-file require a non-empty path."));
610
+ process.exit(1);
611
+ }
612
+ const unsupported = [opts.at ? "--at" : null, opts.in ? "--in" : null].filter(Boolean);
566
613
  if (unsupported.length > 0) {
567
614
  console.error(chalk.red(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} only supported for 1:1 DMs, not channel messages.`));
568
615
  process.exit(1);
@@ -599,21 +646,16 @@ async function runChannelSend(channelName, message, opts) {
599
646
  // and posts the text literally.
600
647
  let mentions = [];
601
648
  if (opts.mentions !== false) {
602
- const tokens = parseMentionTokens(body);
603
- if (tokens.length > 0) {
604
- const members = await fetchChannelMembers(token, channel.channelId);
605
- const resolved = resolveMentions(tokens, members);
606
- if (!resolved.ok) {
607
- console.error(chalk.red(resolved.error));
608
- process.exit(1);
609
- return;
610
- }
611
- mentions = resolved.mentions;
649
+ const resolved = await resolveChannelBodyMentions(token, channel.channelId, body);
650
+ if (!resolved.ok) {
651
+ console.error(chalk.red(resolved.error));
652
+ process.exit(1);
653
+ return;
612
654
  }
655
+ body = resolved.body;
656
+ mentions = resolved.mentions;
613
657
  }
614
658
  // Send the human-readable form: the quotes only existed for parsing.
615
- if (mentions.length > 0)
616
- body = stripMentionQuotes(body);
617
659
  let audience;
618
660
  if (opts.audience !== undefined && opts.audience !== "") {
619
661
  if (!DM_AUDIENCE_VALUES.includes(opts.audience)) {
@@ -622,6 +664,12 @@ async function runChannelSend(channelName, message, opts) {
622
664
  audience = opts.audience;
623
665
  }
624
666
  const payload = { body };
667
+ const prompt = opts.promptFile ? readFileSync(opts.promptFile, "utf8") : opts.prompt;
668
+ const details = opts.detailsFile ? readFileSync(opts.detailsFile, "utf8") : opts.details;
669
+ if (prompt !== undefined)
670
+ payload.prompt = prompt;
671
+ if (details !== undefined)
672
+ payload.details = details;
625
673
  if (mentions.length > 0)
626
674
  payload.mentions = mentions;
627
675
  if (rootEventId)
@@ -736,6 +784,130 @@ async function runDmSend(recipient, message, opts) {
736
784
  process.exit(1);
737
785
  }
738
786
  }
787
+ async function resolveForwardSourceConversation(token, source) {
788
+ if (source.includes(",")) {
789
+ throw new Error("--from accepts one person or channel, not a group recipient list.");
790
+ }
791
+ const channelName = parseChannelName(source);
792
+ if (channelName !== null) {
793
+ const channel = await resolveChannel(token, channelName);
794
+ return { conversationId: channel.channelId };
795
+ }
796
+ const recipient = detectRecipient(source);
797
+ if (!recipient) {
798
+ throw new Error("--from must be a person email, person/agent UID, or channel name/ID.");
799
+ }
800
+ if (recipient.toPersonUid)
801
+ return { conversationId: recipient.toPersonUid };
802
+ const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
803
+ if (!res.ok)
804
+ throw new Error("Could not resolve the source person; use their person UID.");
805
+ const data = (await res.json());
806
+ const peer = (data.contacts ?? []).find((contact) => contact.email?.trim().toLowerCase() === recipient.toEmail);
807
+ if (!peer?.personUid)
808
+ throw new Error("Source person not found in your contacts; use their person UID.");
809
+ return { conversationId: peer.personUid };
810
+ }
811
+ async function runDmForward(eventId, opts) {
812
+ try {
813
+ if (!dmForwardCliEnabled()) {
814
+ console.error(chalk.red("Message forwarding is disabled by the dm.forward-cli kill switch."));
815
+ process.exit(1);
816
+ }
817
+ if (opts.grantFiles && opts.omitFiles) {
818
+ console.error(chalk.red("Choose only one of --grant-files or --omit-files."));
819
+ process.exit(1);
820
+ }
821
+ if (opts.note !== undefined && opts.noteFile !== undefined) {
822
+ console.error(chalk.red("Choose only one of --note or --note-file."));
823
+ process.exit(1);
824
+ }
825
+ if (!eventId.trim()) {
826
+ console.error(chalk.red("A source eventId is required."));
827
+ process.exit(1);
828
+ }
829
+ if (opts.to.includes(",")) {
830
+ console.error(chalk.red("--to accepts one recipient or channel, not a group recipient list."));
831
+ process.exit(1);
832
+ }
833
+ const note = opts.noteFile !== undefined
834
+ ? readFileSync(opts.noteFile, "utf8")
835
+ : opts.note;
836
+ const token = await ensureCognitoToken();
837
+ const source = await resolveForwardSourceConversation(token, opts.from);
838
+ const destinationChannelName = parseChannelName(opts.to);
839
+ const fileAccess = opts.grantFiles ? "grant" : opts.omitFiles ? "omit" : undefined;
840
+ const shared = {
841
+ ...(note !== undefined ? { body: note } : {}),
842
+ forwardOf: { conversationId: source.conversationId, eventId: eventId.trim() },
843
+ ...(fileAccess ? { fileAccess } : {}),
844
+ ...(opts.crossCompany ? { acknowledgeCrossCompany: true } : {}),
845
+ };
846
+ let path;
847
+ let payload;
848
+ let destinationLabel;
849
+ if (destinationChannelName !== null) {
850
+ const channel = await resolveChannel(token, destinationChannelName);
851
+ path = `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/messages`;
852
+ payload = { ...shared };
853
+ if (note !== undefined) {
854
+ const resolved = await resolveChannelBodyMentions(token, channel.channelId, note);
855
+ if (!resolved.ok) {
856
+ console.error(chalk.red(resolved.error));
857
+ process.exit(1);
858
+ return;
859
+ }
860
+ payload.body = resolved.body;
861
+ if (resolved.mentions.length > 0)
862
+ payload.mentions = resolved.mentions;
863
+ }
864
+ destinationLabel = `#${channel.name ?? destinationChannelName}`;
865
+ }
866
+ else {
867
+ const recipient = detectRecipient(opts.to);
868
+ if (!recipient) {
869
+ console.error(chalk.red("--to must be a person email, person/agent UID, or channel name/ID."));
870
+ process.exit(1);
871
+ }
872
+ path = "/v1/notify/dm";
873
+ payload = { ...recipient, ...shared };
874
+ destinationLabel = opts.to;
875
+ }
876
+ const res = await vaultApiFetch({ token, path, method: "POST", body: payload });
877
+ if (!res.ok) {
878
+ const err = (await res.json().catch(() => ({})));
879
+ const code = typeof err.code === "string" ? err.code : undefined;
880
+ if (code === "FORWARD_FILE_ACCESS_REQUIRED") {
881
+ console.error(chalk.yellow(friendlyForwardError(code)));
882
+ const files = Array.isArray(err.files) ? err.files : [];
883
+ for (const item of files) {
884
+ if (!item || typeof item !== "object")
885
+ continue;
886
+ const file = item;
887
+ const label = typeof file.name === "string" ? file.name : "forwarded file";
888
+ const location = typeof file.vaultPath === "string" ? file.vaultPath :
889
+ typeof file.path === "string" ? file.path : undefined;
890
+ console.error(chalk.dim(` - ${label}${location ? ` (${location})` : ""}`));
891
+ }
892
+ console.error(chalk.dim("Retry with --grant-files or --omit-files."));
893
+ }
894
+ else {
895
+ console.error(chalk.red(friendlyForwardError(code)));
896
+ }
897
+ process.exit(1);
898
+ }
899
+ const data = (await res.json().catch(() => ({})));
900
+ if (typeof data.eventId !== "string" || !data.eventId.trim()) {
901
+ console.error(chalk.red("Forward succeeded without returning an eventId."));
902
+ process.exit(1);
903
+ }
904
+ console.log(chalk.green(`Message forwarded to ${destinationLabel}. New eventId: ${data.eventId}`));
905
+ }
906
+ catch (err) {
907
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
908
+ process.exit(1);
909
+ }
910
+ }
739
911
  async function runDmRequests() {
740
912
  try {
741
913
  const token = await ensureCognitoToken();
@@ -871,7 +1043,11 @@ export function formatThreadMessage(m, nowMs) {
871
1043
  const who = m.direction === "out" ? "you" : senderLabel(m);
872
1044
  const when = chalk.dim(formatRelativeTime(m.createdAt, nowMs));
873
1045
  const audienceTag = m.audience === "agent" ? chalk.dim(" [agent]") : "";
874
- return `${arrow} ${chalk.bold(who)} ${when}${audienceTag}\n ${firstLine(m.body)}${formatAttachments(m)}`;
1046
+ const id = m.eventId ? ` ${chalk.dim(m.eventId)}` : "";
1047
+ const forwarded = m.forwardedFrom?.senderName
1048
+ ? `\n ${chalk.dim(`Forwarded from ${m.forwardedFrom.senderName}`)}`
1049
+ : "";
1050
+ return `${arrow} ${chalk.bold(who)} ${when}${audienceTag}${id}${forwarded}\n ${firstLine(m.body)}${formatAttachments(m)}`;
875
1051
  }
876
1052
  /** Render one channel/group message line. */
877
1053
  export function formatChannelMessage(m, nowMs, opts = {}) {
@@ -886,7 +1062,10 @@ export function formatChannelMessage(m, nowMs, opts = {}) {
886
1062
  (senderLabel(m).toLowerCase() === "hq"
887
1063
  ? "System notification (no text provided)."
888
1064
  : "(no message text)");
889
- return `${indent}${who} ${when}${idPart}\n${bodyIndent}${readableBody}${formatAttachments(m, bodyIndent)}`;
1065
+ const forwarded = m.forwardedFrom?.senderName
1066
+ ? `${bodyIndent}${chalk.dim(`Forwarded from ${m.forwardedFrom.senderName}`)}\n`
1067
+ : "";
1068
+ return `${indent}${who} ${when}${idPart}\n${forwarded}${bodyIndent}${readableBody}${formatAttachments(m, bodyIndent)}`;
890
1069
  }
891
1070
  /**
892
1071
  * Read the caller's person/agent uid and email from a JWT we already hold.
@@ -1330,16 +1509,29 @@ export function registerDmCommand(program) {
1330
1509
  .option("--channel <name>", "Post the message to one of your DM channels by name (e.g. --channel vyg-dev)")
1331
1510
  .option("--no-mentions", "Do not resolve `@name` in a channel message into structured mentions — post the text literally")
1332
1511
  .option("--thread <eventId>", 'Reply in a channel thread. Pass a root eventId from `hq dm channel`, or `last` for your most recent top-level message in that channel')
1333
- .option("--prompt <text>", "Agent-context prompt the recipient can one-click copy into their agent (1:1 DMs only)")
1334
- .option("--prompt-file <path>", "Read the agent prompt from a file (1:1 DMs only)")
1335
- .option("--details <text>", "Longer detail shown in the recipient's DM detail window (1:1 DMs only)")
1336
- .option("--details-file <path>", "Read the details from a file (1:1 DMs only)")
1512
+ .option("--prompt <text>", "Agent-context prompt the recipient can one-click copy into their agent (1:1 DMs; channel sends also supported)")
1513
+ .option("--prompt-file <path>", "Read the agent prompt from a file (1:1 DMs; channel sends also supported)")
1514
+ .option("--details <text>", "Longer detail shown in the recipient's DM detail window (1:1 DMs; channel sends also supported)")
1515
+ .option("--details-file <path>", "Read the details from a file (1:1 DMs; channel sends also supported)")
1337
1516
  .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
1338
1517
  .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
1339
1518
  .option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both")
1340
1519
  .action(async (recipient, message, opts) => {
1341
1520
  await runDmSend(recipient, message, opts);
1342
1521
  });
1522
+ dm
1523
+ .command("forward <eventId>")
1524
+ .description("Forward a DM or channel message through the existing send routes.")
1525
+ .requiredOption("--from <person|channel>", "Source person email/UID or channel name/ID")
1526
+ .requiredOption("--to <person|agent|channel>", "Destination person email/UID, agent UID, or channel name/ID")
1527
+ .option("--note <text>", "Optional note to add above the forwarded message")
1528
+ .option("--note-file <path>", "Read the optional forward note from a file")
1529
+ .option("--grant-files", "Grant the recipient access to files they cannot currently open")
1530
+ .option("--omit-files", "Omit files the recipient cannot currently open")
1531
+ .option("--cross-company", "Confirm forwarding across company boundaries")
1532
+ .action(async (eventId, opts) => {
1533
+ await runDmForward(eventId, opts);
1534
+ });
1343
1535
  dm
1344
1536
  .command("inbox")
1345
1537
  .description("List your recent incoming direct messages.")
@@ -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. */