@indigoai-us/hq-cli 5.234.2 → 5.236.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/assets/bot-workers/setup/skills/agents-and-team.md +154 -0
  3. package/assets/bot-workers/setup/skills/first-company.md +4 -1
  4. package/assets/bot-workers/setup/skills/pricing.md +92 -0
  5. package/assets/bot-workers/setup/skills/standing-help.md +7 -0
  6. package/assets/bot-workers/setup/worker.yaml +244 -63
  7. package/dist/command-catalog.generated.d.ts +3 -0
  8. package/dist/command-catalog.generated.js +4 -0
  9. package/dist/commands/bot.d.ts +2 -0
  10. package/dist/commands/bot.js +14 -2
  11. package/dist/commands/lanes-checks.d.ts +0 -3
  12. package/dist/commands/lanes-checks.js +22 -40
  13. package/dist/commands/lanes-import.d.ts +0 -2
  14. package/dist/commands/lanes-import.js +3 -7
  15. package/dist/commands/lanes-report.js +5 -12
  16. package/dist/commands/lanes-tracker.d.ts +0 -3
  17. package/dist/commands/lanes-tracker.js +41 -17
  18. package/dist/commands/lanes.d.ts +0 -5
  19. package/dist/commands/lanes.js +3 -329
  20. package/dist/lib/bot/config.d.ts +12 -0
  21. package/dist/lib/bot/config.js +25 -0
  22. package/dist/lib/bot/local-config.d.ts +2 -1
  23. package/dist/lib/bot/local-config.js +4 -0
  24. package/dist/lib/bot/progress.d.ts +25 -0
  25. package/dist/lib/bot/progress.js +39 -1
  26. package/dist/lib/bot/prompt.d.ts +2 -0
  27. package/dist/lib/bot/prompt.js +12 -2
  28. package/dist/lib/bot/run.js +10 -2
  29. package/dist/lib/lanes/reconcile.d.ts +0 -2
  30. package/dist/lib/lanes/reconcile.js +0 -16
  31. package/dist/lib/lanes/store.js +1 -9
  32. package/dist/lib/lanes/tracker-store.d.ts +2 -6
  33. package/dist/lib/lanes/tracker-store.js +1 -1
  34. package/dist/lib/lanes/types.d.ts +0 -4
  35. package/dist/node-network-compat.d.ts +1 -10
  36. package/dist/node-network-compat.js +3 -70
  37. package/package.json +1 -1
  38. package/dist/lib/lanes/tracker-mesh-questions.d.ts +0 -54
  39. package/dist/lib/lanes/tracker-mesh-questions.js +0 -138
  40. package/dist/lib/lanes/tracker-mesh.d.ts +0 -19
  41. package/dist/lib/lanes/tracker-mesh.js +0 -251
  42. package/dist/node-network-compat-flag.d.ts +0 -19
  43. package/dist/node-network-compat-flag.js +0 -87
@@ -27,6 +27,9 @@ export const PROGRESS_COALESCE_MS = 2_000;
27
27
  export const PROGRESS_MIN_GAP_MS = 4_000;
28
28
  export const WORKING_NOTICE_MS = 60_000;
29
29
  export const WORKING_NOTICE_TEXT = "Working on it — I'll reply here when it's done.";
30
+ /** The setup bot's heads-up, sent once when a turn runs past SETUP_WORKING_NOTICE_MS. */
31
+ export const SETUP_WORKING_NOTICE_MS = 30_000;
32
+ export const SETUP_WORKING_NOTICE_TEXT = "This one takes a minute. I'll reply here as soon as it's done.";
30
33
  /** Whitespace-insensitive form used to recognise repeated text. */
31
34
  export function normalizeForCompare(text) {
32
35
  return text.replace(/\s+/g, "");
@@ -46,6 +49,9 @@ export class ProgressPoster {
46
49
  workingNoticeMs;
47
50
  workingNoticeText;
48
51
  buffer;
52
+ dropPreTool;
53
+ /** dropPreToolMessages: messages waiting to learn whether a tool call follows. */
54
+ candidates = [];
49
55
  pending = [];
50
56
  /** In-between questions, held until the turn ends (see the header). */
51
57
  heldQuestions = [];
@@ -58,6 +64,13 @@ export class ProgressPoster {
58
64
  finished = false;
59
65
  noticeSent = false;
60
66
  delivered = 0;
67
+ /**
68
+ * Posts handed to the send chain this turn, counted when queued rather than
69
+ * when delivered. The "(no reply)" fallback checks this: a post still in
70
+ * flight when the turn ends has not bumped `delivered` yet, and counting only
71
+ * deliveries posted the placeholder right after a real message.
72
+ */
73
+ queued = 0;
61
74
  constructor(opts) {
62
75
  this.send = opts.send;
63
76
  this.sendFinalFn = opts.sendFinal;
@@ -69,6 +82,7 @@ export class ProgressPoster {
69
82
  this.workingNoticeMs = opts.workingNoticeMs ?? WORKING_NOTICE_MS;
70
83
  this.workingNoticeText = opts.workingNoticeText ?? WORKING_NOTICE_TEXT;
71
84
  this.buffer = opts.buffer ?? false;
85
+ this.dropPreTool = opts.dropPreToolMessages ?? false;
72
86
  }
73
87
  /** Arm the one-time "working on it" note. Call when the turn starts. */
74
88
  start() {
@@ -93,6 +107,27 @@ export class ProgressPoster {
93
107
  // silence signal, not content, and must never be posted as progress.
94
108
  if (containsNoReplySentinel(t))
95
109
  return;
110
+ if (this.dropPreTool) {
111
+ // The previous message was not followed by a tool call: it was real.
112
+ this.promoteCandidates();
113
+ this.candidates.push(t);
114
+ return;
115
+ }
116
+ this.accept(t);
117
+ }
118
+ /** The model called a tool: whatever it wrote just before was a note to itself. */
119
+ tool() {
120
+ if (this.finished || !this.dropPreTool)
121
+ return;
122
+ this.candidates = [];
123
+ }
124
+ promoteCandidates() {
125
+ const kept = this.candidates;
126
+ this.candidates = [];
127
+ for (const t of kept)
128
+ this.accept(t);
129
+ }
130
+ accept(t) {
96
131
  this.cancelNotice();
97
132
  this.said.push(t);
98
133
  // An unmentioned room turn holds every in-between message: nothing is posted
@@ -124,6 +159,8 @@ export class ProgressPoster {
124
159
  async finish(finalText) {
125
160
  if (this.finished)
126
161
  return this.chain;
162
+ // A message the turn ended on (no tool after it) is real.
163
+ this.promoteCandidates();
127
164
  this.finished = true;
128
165
  this.cancelNotice();
129
166
  this.cancelFlush();
@@ -161,7 +198,7 @@ export class ProgressPoster {
161
198
  const shouldAddFinal = Boolean(final && !this.repeatsSaid(final));
162
199
  if (!shouldAddFinal && held.length > 0)
163
200
  this.pending.push(...held);
164
- if (this.pending.length === 0 && this.delivered === 0 && !shouldAddFinal)
201
+ if (this.pending.length === 0 && this.queued === 0 && !shouldAddFinal)
165
202
  this.pending.push("(no reply)");
166
203
  if (this.sendFinalFn && shouldAddFinal) {
167
204
  // Flush any remaining progress messages first, then the final reply separately.
@@ -229,6 +266,7 @@ export class ProgressPoster {
229
266
  // path built this body.
230
267
  if (containsNoReplySentinel(body))
231
268
  return;
269
+ this.queued += 1;
232
270
  this.lastPostAt = this.now();
233
271
  this.chain = this.chain.then(async () => {
234
272
  try {
@@ -77,6 +77,8 @@ export declare function normalizeInstructions(raw: unknown): string | undefined;
77
77
  export declare function readPromptSources(hqRoot: string, workerDir: string, io?: PromptFs, opts?: ReadPromptSourcesOptions): PromptSources;
78
78
  export interface BuildSystemPromptInput {
79
79
  botName: string;
80
+ /** The human name people call the bot (e.g. "Maya"); unset = botName. */
81
+ displayName?: string;
80
82
  agentUid: string;
81
83
  ownerUid: string;
82
84
  hqRoot: string;
@@ -229,6 +229,16 @@ export function readPromptSources(hqRoot, workerDir, io = defaultPromptFs, opts
229
229
  }
230
230
  return { workerYaml, persona: read("persona.md"), memory };
231
231
  }
232
+ /**
233
+ * How the bot names itself: its display name when it has one ("Maya", with the
234
+ * handle kept for commands), else its handle.
235
+ */
236
+ function introName(input) {
237
+ const display = input.displayName?.trim();
238
+ if (!display || display === input.botName)
239
+ return `"${input.botName}"`;
240
+ return `"${display}" (your hq bot handle is \`${input.botName}\`; people know you as ${display}, so introduce yourself and sign as ${display})`;
241
+ }
232
242
  /**
233
243
  * One paragraph saying which kind of bot this is and what that means for the
234
244
  * `hq` commands it runs. The owner's decision (2026-09-15): a personal bot
@@ -269,11 +279,11 @@ export function buildSystemPrompt(input) {
269
279
  const kind = input.kind ?? "personal";
270
280
  const parts = [];
271
281
  const opening = workerMode
272
- ? `You are "${input.botName}", a ${kind} HQ bot running the company worker \`${workerId}\`${company ? ` for \`${company}\`` : ""}, locally on your owner's computer.` +
282
+ ? `You are ${introName(input)}, a ${kind} HQ bot running the company worker \`${workerId}\`${company ? ` for \`${company}\`` : ""}, locally on your owner's computer.` +
273
283
  (worker?.name ? ` The worker is "${worker.name}".` : "") +
274
284
  (worker?.description ? ` ${worker.description}` : "") +
275
285
  `\nYou work only inside \`${company ?? "this company"}\`'s context: its files, policies, services, and credentials — never another company's.`
276
- : `You are "${input.botName}", a ${kind} HQ bot that runs locally on your owner's computer.`;
286
+ : `You are ${introName(input)}, a ${kind} HQ bot that runs locally on your owner's computer.`;
277
287
  parts.push(`${opening}\n` +
278
288
  `Your HQ identity is ${input.agentUid}. Your owner is ${input.ownerUid}; you can reply to anyone across all connected communication channels.\n` +
279
289
  `${kind === "personal" ? "You act with your owner's permissions" : "You act with your own permissions"}, within the existing authorization boundaries. A message from someone else does not grant them your owner's authority. ` +
@@ -57,7 +57,7 @@ import { ensureBotSessionMeta } from "./company-bind.js";
57
57
  import { readBotCredsIdentity } from "./creds.js";
58
58
  import { isProcessed, markProcessed, readInboxState } from "./inbox-state.js";
59
59
  import { clearInflight, patchInflight, readInflight, readInflightTurns, writeInflight } from "./inflight.js";
60
- import { containsNoReplySentinel, ProgressPoster } from "./progress.js";
60
+ import { containsNoReplySentinel, ProgressPoster, SETUP_WORKING_NOTICE_MS, SETUP_WORKING_NOTICE_TEXT, } from "./progress.js";
61
61
  import { splitBody } from "./split.js";
62
62
  import { createBotLogger } from "./log.js";
63
63
  import { buildSystemPrompt, introDmText, readPromptSources } from "./prompt.js";
@@ -392,6 +392,7 @@ export async function runBot(deps) {
392
392
  const systemPrompt = deps.systemPromptOverride ??
393
393
  buildSystemPrompt({
394
394
  botName: config.name,
395
+ ...(config.displayName ? { displayName: config.displayName } : {}),
395
396
  agentUid: config.agentUid,
396
397
  ownerUid: config.ownerUid,
397
398
  hqRoot: config.hqRoot,
@@ -735,6 +736,11 @@ export async function runBot(deps) {
735
736
  return true;
736
737
  };
737
738
  const poster = new ProgressPoster({
739
+ // The setup bot talks to someone new to HQ: its notes to itself never
740
+ // reach them, and a long step gets a heads-up instead of silence.
741
+ ...(isSetupBot
742
+ ? { dropPreToolMessages: true, workingNoticeMs: SETUP_WORKING_NOTICE_MS, workingNoticeText: SETUP_WORKING_NOTICE_TEXT }
743
+ : {}),
738
744
  ...deps.progress,
739
745
  ...(args.workingNotice === false || silenceable ? { workingNoticeMs: 0 } : {}),
740
746
  ...(silenceable ? { buffer: true } : {}),
@@ -749,6 +755,8 @@ export async function runBot(deps) {
749
755
  onEvent: (event) => {
750
756
  if (event.kind === "message")
751
757
  poster.message(event.text);
758
+ else if (event.kind === "tool")
759
+ poster.tool();
752
760
  },
753
761
  signal: controller.signal,
754
762
  ...(args.readDirs?.length ? { readDirs: args.readDirs } : {}),
@@ -1450,7 +1458,7 @@ export async function runBot(deps) {
1450
1458
  tReceived,
1451
1459
  send,
1452
1460
  sendFinal,
1453
- workingNotice: !isSetupBot,
1461
+ // The setup bot's own notice (SETUP_WORKING_NOTICE_TEXT) is set where the poster is built.
1454
1462
  silenceable: target.kind === "room" && !target.mentioned,
1455
1463
  ...(readDirs.length ? { readDirs } : {}),
1456
1464
  ...(target.kind === "room"
@@ -81,8 +81,6 @@ export interface ReconcileDeps {
81
81
  readRegistryFlag?: typeof readLanesRegistryFlag;
82
82
  /** Active-company fallback for questions raised from an unscoped lane. */
83
83
  resolveQuestionActiveCompany?: () => Promise<LaneCompanyRef>;
84
- /** Index local questions and poll remote answers before the normal answer-wake pass. */
85
- syncTrackerQuestions?: (hqRoot: string) => Promise<void>;
86
84
  /** Resume queued messages after this pass wins a local dead-worker CAS. */
87
85
  recoverQueuedMessages?: (lane: LaneRecord) => Promise<void>;
88
86
  /** Detached mesh seam for dead-watcher recovery. */
@@ -632,14 +632,6 @@ export async function runPassBody(hqRoot, deps) {
632
632
  const livenessDeps = deps.livenessDeps ?? defaultLivenessDeps();
633
633
  let lanes;
634
634
  let questions;
635
- if (!deps.dryRun) {
636
- try {
637
- await deps.syncTrackerQuestions?.(hqRoot);
638
- }
639
- catch (err) {
640
- markError(summary, err);
641
- }
642
- }
643
635
  try {
644
636
  lanes = loadLaneRecords(hqRoot, summary);
645
637
  questions = loadQuestionRecords(hqRoot, summary);
@@ -843,14 +835,6 @@ export async function runPassBody(hqRoot, deps) {
843
835
  await admitQueued(hqRoot, drainLanes, summary, deps, now);
844
836
  await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, deps);
845
837
  });
846
- if (!deps.dryRun && deps.syncTrackerQuestions) {
847
- try {
848
- await deps.syncTrackerQuestions(hqRoot);
849
- }
850
- catch (err) {
851
- markError(summary, err);
852
- }
853
- }
854
838
  if (!deps.dryRun && summary.errors.length === 0) {
855
839
  try {
856
840
  writeHeartbeatFile(hqRoot, summary, nowIso);
@@ -738,15 +738,7 @@ export function parseQuestionRecord(raw, filePath) {
738
738
  value: delivered_at,
739
739
  });
740
740
  }
741
- const mesh_question_id = raw.mesh_question_id;
742
- if (mesh_question_id !== undefined && (typeof mesh_question_id !== "string" || !mesh_question_id.trim())) {
743
- throw new LaneSchemaError("mesh_question_id must be a non-empty string when present", { path: filePath, value: mesh_question_id });
744
- }
745
- const mesh_delivered_at = raw.mesh_delivered_at === undefined ? null : raw.mesh_delivered_at;
746
- if (mesh_delivered_at !== null && (typeof mesh_delivered_at !== "string" || !Number.isFinite(Date.parse(mesh_delivered_at)))) {
747
- throw new LaneSchemaError("mesh_delivered_at must be a valid timestamp or null", { path: filePath, value: mesh_delivered_at });
748
- }
749
- return { ...raw, delivered_at, mesh_delivered_at };
741
+ return { ...raw, delivered_at };
750
742
  }
751
743
  /**
752
744
  * HQ Sync sibling-twin grammar from `@indigoai-us/hq-cloud` `buildConflictPath`:
@@ -22,17 +22,13 @@ export interface TrackerStore {
22
22
  read(kind: TrackerKind, id: string): StoreValue<TrackerRecord | null>;
23
23
  list(kind: TrackerKind): StoreValue<TrackerRecord[]>;
24
24
  create(record: TrackerRecordInput): StoreValue<TrackerRecord>;
25
- update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord, metadata?: {
26
- approvalQuestionId?: string;
27
- }): StoreValue<TrackerRecord>;
25
+ update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord): StoreValue<TrackerRecord>;
28
26
  }
29
27
  export interface LocalTrackerStore extends TrackerStore {
30
28
  read(kind: TrackerKind, id: string): TrackerRecord | null;
31
29
  list(kind: TrackerKind): TrackerRecord[];
32
30
  create(record: TrackerRecordInput): TrackerRecord;
33
- update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord, metadata?: {
34
- approvalQuestionId?: string;
35
- }): TrackerRecord;
31
+ update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord): TrackerRecord;
36
32
  }
37
33
  export declare function createLocalTrackerStore(projectDirectory: string, now?: () => Date): LocalTrackerStore;
38
34
  //# sourceMappingURL=tracker-store.d.ts.map
@@ -238,7 +238,7 @@ export function createLocalTrackerStore(projectDirectory, now = () => new Date()
238
238
  writeExclusive(path.join(directory, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`);
239
239
  return record;
240
240
  },
241
- update(kind, id, expectedVersion, change, _metadata) {
241
+ update(kind, id, expectedVersion, change) {
242
242
  const file = recordPath(root, kind, id);
243
243
  const directory = ensureKindDirectory(root, kind);
244
244
  return withRecordLock(file, () => {
@@ -662,10 +662,6 @@ export interface QuestionRecord {
662
662
  version: number;
663
663
  /** Null until the owner starts a resume carrying this answer; absent on old records. */
664
664
  delivered_at?: string | null;
665
- /** Work Mesh's server-issued id; absent for pre-TRK-9 local questions. */
666
- mesh_question_id?: string;
667
- /** Local checkpoint that the server accepted this question's delivery stamp. */
668
- mesh_delivered_at?: string | null;
669
665
  body: QuestionBodyRef;
670
666
  routing_history: RoutingHistoryEntry[];
671
667
  answer?: QuestionAnswer;
@@ -1,28 +1,19 @@
1
1
  /**
2
2
  * Node network defaults for HQ CLI entry points.
3
3
  *
4
- * The compatibility timeout remains the default. On a canary where IPv6 is
5
- * advertised but unusable, the default-off hq-flags gate can also prefer IPv4
6
- * and disable automatic family racing before the CLI issues its first request.
4
+ * The compatibility timeout remains the default for HQ CLI entry points.
7
5
  *
8
6
  * Keep this module safe to import immediately after the Node version preflight.
9
- * The hq-flags reader loads dynamically so an unsupported runtime exits through
10
- * the preflight rather than failing during module linking.
11
7
  */
12
8
  export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
13
9
  export interface NodeNetworkCompatibilityOptions {
14
10
  nodeOptions?: string;
15
11
  execArgv?: readonly string[];
16
- forceIpv4?: boolean;
17
12
  /**
18
13
  * Inject the net timeout setter (tests). Pass null to simulate a runtime
19
14
  * that lacks setDefaultAutoSelectFamilyAttemptTimeout.
20
15
  */
21
16
  setAttemptTimeout?: ((milliseconds: number) => void) | null;
22
- /** Inject Node's automatic family selection setter (tests). */
23
- setAutoSelectFamily?: ((enabled: boolean) => void) | null;
24
- /** Inject Node's DNS result-order setter (tests). */
25
- setDefaultResultOrder?: ((order: "ipv4first") => void) | null;
26
17
  }
27
18
  export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
28
19
  //# sourceMappingURL=node-network-compat.d.ts.map
@@ -1,15 +1,10 @@
1
1
  /**
2
2
  * Node network defaults for HQ CLI entry points.
3
3
  *
4
- * The compatibility timeout remains the default. On a canary where IPv6 is
5
- * advertised but unusable, the default-off hq-flags gate can also prefer IPv4
6
- * and disable automatic family racing before the CLI issues its first request.
4
+ * The compatibility timeout remains the default for HQ CLI entry points.
7
5
  *
8
6
  * Keep this module safe to import immediately after the Node version preflight.
9
- * The hq-flags reader loads dynamically so an unsupported runtime exits through
10
- * the preflight rather than failing during module linking.
11
7
  */
12
- import * as nodeDns from "node:dns";
13
8
  import * as nodeNet from "node:net";
14
9
  export const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
15
10
  const ATTEMPT_TIMEOUT_OPTION = "--network-family-autoselection-attempt-timeout";
@@ -19,18 +14,6 @@ function hasExplicitAttemptTimeout(nodeOptions, execArgv) {
19
14
  execArgv.some((argument) => argument === ATTEMPT_TIMEOUT_OPTION ||
20
15
  argument.startsWith(ATTEMPT_TIMEOUT_OPTION + "=")));
21
16
  }
22
- function hasExplicitFamilySelection(nodeOptions, execArgv) {
23
- const optionPattern = /(?:^|\s)(?:--network-family-autoselection|--enable-network-family-autoselection|--no-network-family-autoselection|--dns-result-order)(?:=|\s|$)/;
24
- return (optionPattern.test(nodeOptions) ||
25
- execArgv.some((argument) => argument === "--network-family-autoselection" ||
26
- argument === "--enable-network-family-autoselection" ||
27
- argument === "--no-network-family-autoselection" ||
28
- argument === "--dns-result-order" ||
29
- argument.startsWith("--network-family-autoselection=") ||
30
- argument.startsWith("--enable-network-family-autoselection=") ||
31
- argument.startsWith("--no-network-family-autoselection=") ||
32
- argument.startsWith("--dns-result-order=")));
33
- }
34
17
  function resolveSetAttemptTimeout(override) {
35
18
  if (override === null)
36
19
  return undefined;
@@ -39,69 +22,19 @@ function resolveSetAttemptTimeout(override) {
39
22
  const fn = nodeNet.setDefaultAutoSelectFamilyAttemptTimeout;
40
23
  return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
41
24
  }
42
- function resolveSetAutoSelectFamily(override) {
43
- if (override === null)
44
- return undefined;
45
- if (override)
46
- return override;
47
- const fn = nodeNet.setDefaultAutoSelectFamily;
48
- return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
49
- }
50
- function resolveSetDefaultResultOrder(override) {
51
- if (override === null)
52
- return undefined;
53
- if (override)
54
- return override;
55
- const fn = nodeDns.setDefaultResultOrder;
56
- return typeof fn === "function" ? fn.bind(nodeDns) : undefined;
57
- }
58
- function applyIpv4Preference(options, nodeOptions, execArgv) {
59
- if (!options.forceIpv4 || hasExplicitFamilySelection(nodeOptions, execArgv)) {
60
- return false;
61
- }
62
- const setAutoSelectFamily = resolveSetAutoSelectFamily(options.setAutoSelectFamily);
63
- const setDefaultResultOrder = resolveSetDefaultResultOrder(options.setDefaultResultOrder);
64
- if (!setAutoSelectFamily || !setDefaultResultOrder)
65
- return false;
66
- setDefaultResultOrder("ipv4first");
67
- setAutoSelectFamily(false);
68
- return true;
69
- }
70
25
  export function configureNodeNetworkCompatibility(options = {}) {
71
26
  const nodeOptions = options.nodeOptions ?? process.env.NODE_OPTIONS ?? "";
72
27
  const execArgv = options.execArgv ?? process.execArgv;
73
- const ipv4Applied = applyIpv4Preference(options, nodeOptions, execArgv);
74
28
  if (hasExplicitAttemptTimeout(nodeOptions, execArgv)) {
75
- return ipv4Applied;
29
+ return false;
76
30
  }
77
31
  const setAttemptTimeout = resolveSetAttemptTimeout(options.setAttemptTimeout);
78
32
  if (!setAttemptTimeout) {
79
33
  // Runtime lacks the API; the Node preflight should already have exited.
80
- return ipv4Applied;
34
+ return false;
81
35
  }
82
36
  setAttemptTimeout(HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS);
83
37
  return true;
84
38
  }
85
- async function applyHqFlaggedIpv4Preference() {
86
- try {
87
- const { nodeNetworkForceIpv4Enabled } = await import("./node-network-compat-flag.js");
88
- if (nodeNetworkForceIpv4Enabled &&
89
- (await nodeNetworkForceIpv4Enabled())) {
90
- configureNodeNetworkCompatibility({ forceIpv4: true });
91
- }
92
- }
93
- catch (error) {
94
- const rawName = error instanceof Error ? error.name : "UnknownError";
95
- const errorName = /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(rawName)
96
- ? rawName
97
- : "UnknownError";
98
- process.stderr.write("HQ network IPv4 flag lookup failed (" +
99
- errorName +
100
- "); using default off.\n");
101
- }
102
- }
103
- // Preserve the historical timeout before any flag request. If the hq-flags
104
- // lookup succeeds, the IPv4 preference is applied before command dispatch.
105
39
  configureNodeNetworkCompatibility();
106
- await applyHqFlaggedIpv4Preference();
107
40
  //# sourceMappingURL=node-network-compat.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.234.2",
3
+ "version": "5.236.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,54 +0,0 @@
1
- import type { LaneCompanyRef, QuestionRecord } from "./types.js";
2
- import { type TrackerMeshRequest } from "./tracker-mesh.js";
3
- export type TrackerMeshQuestionAudience = "owner" | "operator" | "lane";
4
- export type TrackerMeshQuestionStatus = "pending" | "answered" | "cancelled";
5
- export interface TrackerMeshQuestionRow {
6
- questionId: string;
7
- threadId: string;
8
- companyUid: string;
9
- projectId: string;
10
- laneId: string;
11
- host: {
12
- kind: string;
13
- id: string;
14
- };
15
- audience: TrackerMeshQuestionAudience;
16
- audienceChain: string[];
17
- routingHistory: Array<Record<string, string>>;
18
- question: string;
19
- context?: string;
20
- options: string[];
21
- recommended?: string;
22
- askedAt: string;
23
- status: TrackerMeshQuestionStatus;
24
- choice?: string;
25
- notes?: string;
26
- answeredBy?: string;
27
- answeredAt?: string;
28
- deliveredAt?: string;
29
- }
30
- export interface TrackerMeshQuestionInput {
31
- laneId: string;
32
- threadId?: string;
33
- projectId: string;
34
- audience: TrackerMeshQuestionAudience;
35
- audienceChain: string[];
36
- routingHistory: Array<Record<string, string>>;
37
- question: string;
38
- context: string;
39
- options: string[];
40
- recommended?: string;
41
- }
42
- export interface TrackerMeshQuestionClient {
43
- create(input: TrackerMeshQuestionInput): Promise<TrackerMeshQuestionRow>;
44
- list(statuses?: readonly TrackerMeshQuestionStatus[]): Promise<TrackerMeshQuestionRow[]>;
45
- answer(questionId: string, choice: string, notes?: string): Promise<TrackerMeshQuestionRow>;
46
- delivered(questionId: string): Promise<TrackerMeshQuestionRow>;
47
- }
48
- /** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
49
- export declare function deliverTrackerMeshQuestionOnce(question: QuestionRecord, company: LaneCompanyRef, client: Pick<TrackerMeshQuestionClient, "delivered">, persist: (deliveredAt: string) => QuestionRecord): Promise<QuestionRecord>;
50
- export declare function createTrackerMeshQuestionClient(options: {
51
- company: LaneCompanyRef;
52
- request?: TrackerMeshRequest;
53
- }): TrackerMeshQuestionClient;
54
- //# sourceMappingURL=tracker-mesh-questions.d.ts.map
@@ -1,138 +0,0 @@
1
- import { meshJson, requireToken } from "../mesh/client.js";
2
- import { LanesError } from "./errors.js";
3
- import { mapWorkMeshTrackerError } from "./tracker-mesh.js";
4
- /** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
5
- export async function deliverTrackerMeshQuestionOnce(question, company, client, persist) {
6
- if (!question.mesh_question_id || question.status !== "answered" ||
7
- !question.delivered_at || question.mesh_delivered_at)
8
- return question;
9
- const delivered = await client.delivered(question.mesh_question_id);
10
- if (delivered.questionId !== question.mesh_question_id ||
11
- delivered.companyUid !== company.uid ||
12
- delivered.laneId !== question.lane_id ||
13
- delivered.projectId !== question.project_id ||
14
- !delivered.deliveredAt) {
15
- throw new LanesError("tracker_mesh_response_invalid", "Work Mesh did not stamp the expected question as delivered", {
16
- mesh_question_id: question.mesh_question_id,
17
- question_id: question.question_id,
18
- });
19
- }
20
- return persist(delivered.deliveredAt);
21
- }
22
- const ROOT = "/v1/work-mesh/tracker/questions";
23
- const AUDIENCES = ["owner", "operator", "lane"];
24
- const STATUSES = ["pending", "answered", "cancelled"];
25
- const MAX_PAGES_PER_FILTER = 10;
26
- function invalidResponse(field) {
27
- throw new LanesError("tracker_mesh_response_invalid", `Work Mesh returned an invalid tracker question ${field}`, { field });
28
- }
29
- function parseQuestion(raw, company) {
30
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
31
- return invalidResponse("row");
32
- const row = raw;
33
- if (typeof row.questionId !== "string" || !row.questionId ||
34
- typeof row.threadId !== "string" || !row.threadId ||
35
- row.companyUid !== company.uid ||
36
- typeof row.projectId !== "string" || !row.projectId ||
37
- typeof row.laneId !== "string" || !row.laneId ||
38
- !AUDIENCES.includes(row.audience) ||
39
- !STATUSES.includes(row.status) ||
40
- typeof row.question !== "string" ||
41
- !Array.isArray(row.options) || row.options.some((entry) => typeof entry !== "string") ||
42
- !Array.isArray(row.audienceChain) || row.audienceChain.some((entry) => typeof entry !== "string") ||
43
- !Array.isArray(row.routingHistory) ||
44
- typeof row.askedAt !== "string" || !Number.isFinite(Date.parse(row.askedAt)))
45
- return invalidResponse("row");
46
- if (row.context !== undefined && typeof row.context !== "string")
47
- return invalidResponse("context");
48
- if (row.recommended !== undefined && typeof row.recommended !== "string")
49
- return invalidResponse("recommended");
50
- if (row.choice !== undefined && typeof row.choice !== "string")
51
- return invalidResponse("choice");
52
- if (row.notes !== undefined && typeof row.notes !== "string")
53
- return invalidResponse("notes");
54
- if (row.answeredBy !== undefined && typeof row.answeredBy !== "string")
55
- return invalidResponse("answeredBy");
56
- if (row.answeredAt !== undefined && (typeof row.answeredAt !== "string" || !Number.isFinite(Date.parse(row.answeredAt))))
57
- return invalidResponse("answeredAt");
58
- if (row.deliveredAt !== undefined && (typeof row.deliveredAt !== "string" || !Number.isFinite(Date.parse(row.deliveredAt))))
59
- return invalidResponse("deliveredAt");
60
- if (!row.host || typeof row.host !== "object" || Array.isArray(row.host) || typeof row.host.kind !== "string" || typeof row.host.id !== "string")
61
- return invalidResponse("host");
62
- return row;
63
- }
64
- export function createTrackerMeshQuestionClient(options) {
65
- const { company } = options;
66
- if (!company.slug.trim() || !company.uid.trim()) {
67
- throw new LanesError("tracker_company_unresolved", "A resolved company slug and uid are required for Work Mesh tracker questions", {});
68
- }
69
- const request = options.request ?? (async (endpoint, init) => {
70
- const token = await requireToken({ interactive: false });
71
- return meshJson(token, endpoint, init);
72
- });
73
- const call = async (endpoint, init) => {
74
- try {
75
- const result = await request(endpoint, init);
76
- if (!result || typeof result !== "object" || Array.isArray(result))
77
- return invalidResponse("response");
78
- return result;
79
- }
80
- catch (err) {
81
- throw mapWorkMeshTrackerError(err);
82
- }
83
- };
84
- return {
85
- async create(input) {
86
- const result = await call(ROOT, { method: "POST", body: { companyUid: company.uid, ...input } });
87
- return parseQuestion(result.question, company);
88
- },
89
- async list(requestedStatuses = STATUSES) {
90
- const uniqueStatuses = [...new Set(requestedStatuses)];
91
- if (uniqueStatuses.some((status) => !STATUSES.includes(status))) {
92
- throw new LanesError("tracker_mesh_query_invalid", "Unsupported Work Mesh tracker question status filter", { statuses: uniqueStatuses });
93
- }
94
- const rows = new Map();
95
- for (const audience of AUDIENCES) {
96
- for (const status of uniqueStatuses) {
97
- let cursor;
98
- for (let page = 0; page < MAX_PAGES_PER_FILTER; page += 1) {
99
- const query = { companyUid: company.uid, audience, status, limit: "100", ...(cursor ? { cursor } : {}) };
100
- const result = await call(ROOT, { method: "GET", query });
101
- if (!Array.isArray(result.questions))
102
- return invalidResponse("questions list");
103
- for (const raw of result.questions) {
104
- const row = parseQuestion(raw, company);
105
- if (row.audience !== audience || row.status !== status)
106
- return invalidResponse("filter scope");
107
- rows.set(row.questionId, row);
108
- }
109
- if (result.nextCursor === undefined)
110
- break;
111
- if (typeof result.nextCursor !== "string" || !result.nextCursor || result.nextCursor === cursor)
112
- return invalidResponse("cursor");
113
- if (page === MAX_PAGES_PER_FILTER - 1) {
114
- throw new LanesError("tracker_mesh_response_incomplete", "Work Mesh tracker question list exceeded its bounded page limit", { audience, status, max_pages: MAX_PAGES_PER_FILTER });
115
- }
116
- cursor = result.nextCursor;
117
- }
118
- }
119
- }
120
- return [...rows.values()].sort((a, b) => b.askedAt.localeCompare(a.askedAt) || a.questionId.localeCompare(b.questionId));
121
- },
122
- async answer(questionId, choice, notes) {
123
- const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/answer`, {
124
- method: "POST",
125
- body: { companyUid: company.uid, choice, ...(notes === undefined ? {} : { notes }) },
126
- });
127
- return parseQuestion(result.question, company);
128
- },
129
- async delivered(questionId) {
130
- const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/delivered`, {
131
- method: "POST",
132
- body: { companyUid: company.uid },
133
- });
134
- return parseQuestion(result.question, company);
135
- },
136
- };
137
- }
138
- //# sourceMappingURL=tracker-mesh-questions.js.map
@@ -1,19 +0,0 @@
1
- import { LanesError } from "./errors.js";
2
- import type { LaneCompanyRef } from "./types.js";
3
- import type { TrackerStore } from "./tracker-store.js";
4
- export interface TrackerMeshRequestInit {
5
- method?: string;
6
- body?: Record<string, unknown>;
7
- query?: Record<string, string>;
8
- }
9
- export type TrackerMeshRequest = (path: string, init: TrackerMeshRequestInit) => Promise<unknown>;
10
- export interface WorkMeshTrackerStoreOptions {
11
- company: LaneCompanyRef;
12
- projectId: string;
13
- request?: TrackerMeshRequest;
14
- now?: () => Date;
15
- clientRecordId?: () => string;
16
- }
17
- export declare function mapWorkMeshTrackerError(err: unknown): LanesError;
18
- export declare function createWorkMeshTrackerStore(options: WorkMeshTrackerStoreOptions): TrackerStore;
19
- //# sourceMappingURL=tracker-mesh.d.ts.map