@indigoai-us/hq-cli 5.342.18 → 5.343.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/command-catalog.generated.d.ts +12 -4
  3. package/dist/command-catalog.generated.js +14 -4
  4. package/dist/commands/company.d.ts +12 -0
  5. package/dist/commands/company.js +103 -6
  6. package/dist/commands/daemon.js +2 -11
  7. package/dist/commands/dm.js +10 -1
  8. package/dist/commands/lanes-shared.d.ts +1 -0
  9. package/dist/commands/lanes.d.ts +1 -0
  10. package/dist/commands/lanes.js +11 -0
  11. package/dist/commands/mesh.js +6 -2
  12. package/dist/commands/pack-install.d.ts +2 -0
  13. package/dist/commands/pack-install.js +7 -0
  14. package/dist/commands/pkg-update.d.ts +2 -0
  15. package/dist/commands/pkg-update.js +5 -1
  16. package/dist/commands/secrets.js +2 -3
  17. package/dist/lib/agent-kit/mcp/tools.js +1 -1
  18. package/dist/lib/bot/api.d.ts +1 -0
  19. package/dist/lib/bot/api.js +2 -0
  20. package/dist/lib/bot/progress.d.ts +12 -14
  21. package/dist/lib/bot/progress.js +29 -40
  22. package/dist/lib/bot/run.js +24 -24
  23. package/dist/lib/core-utils/sentry-report.d.ts +1 -1
  24. package/dist/lib/core-utils/sentry-report.js +18 -1
  25. package/dist/lib/daemon/command-call.d.ts +9 -0
  26. package/dist/lib/daemon/command-call.js +18 -0
  27. package/dist/lib/doctor/checks/work-context.js +5 -0
  28. package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
  29. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  30. package/dist/lib/mesh/client.js +11 -0
  31. package/dist/lib/mesh/denial-codes.d.ts +13 -2
  32. package/dist/lib/mesh/denial-codes.js +25 -1
  33. package/dist/lib/mesh/live/daemon/doctor.js +4 -0
  34. package/dist/lib/work-context/outbox.js +8 -2
  35. package/dist/utils/cli-telemetry.js +5 -1
  36. package/dist/utils/install-tree-torn.d.ts +16 -13
  37. package/dist/utils/install-tree-torn.js +58 -26
  38. package/dist/utils/resident-package-preload.d.ts +1 -1
  39. package/dist/utils/resident-package-preload.js +62 -0
  40. package/dist/utils/secrets-sandbox-resident.d.ts +5 -0
  41. package/dist/utils/secrets-sandbox-resident.js +8 -0
  42. package/package.json +1 -1
@@ -36,8 +36,8 @@ export function containsNoReplySentinel(text) {
36
36
  return /\{\s*"action"\s*:\s*"no_reply"\s*\}/.test(text);
37
37
  }
38
38
  export class ProgressPoster {
39
- send;
40
- sendFinalFn;
39
+ sendProgress;
40
+ sendToPerson;
41
41
  now;
42
42
  setTimer;
43
43
  clearTimer;
@@ -68,9 +68,11 @@ export class ProgressPoster {
68
68
  * deliveries posted the placeholder right after a real message.
69
69
  */
70
70
  queued = 0;
71
+ /** Posts queued for the person, excluding agent-lane status beats. */
72
+ personQueued = 0;
71
73
  constructor(opts) {
72
- this.send = opts.send;
73
- this.sendFinalFn = opts.sendFinal;
74
+ this.sendProgress = opts.sendProgress;
75
+ this.sendToPerson = opts.sendToPerson;
74
76
  this.now = opts.now ?? Date.now;
75
77
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
76
78
  this.clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -92,7 +94,7 @@ export class ProgressPoster {
92
94
  if (this.finished || this.said.length > 0 || this.noticeSent)
93
95
  return;
94
96
  this.noticeSent = true;
95
- this.enqueue(this.workingNoticeText);
97
+ this.enqueueWith(this.workingNoticeText, this.sendProgress);
96
98
  }, this.workingNoticeMs);
97
99
  }
98
100
  /** One finished assistant message from the model. */
@@ -147,11 +149,9 @@ export class ProgressPoster {
147
149
  return this.said.length > 0;
148
150
  }
149
151
  /**
150
- * The turn succeeded: post whatever is pending, then the final answer unless
151
- * it repeats what was already said. Resolves once everything is sent.
152
- *
153
- * When `sendFinal` was provided, progress items flush through `send` and the
154
- * final reply goes through `sendFinal` — keeping their audience tags distinct.
152
+ * The turn succeeded: post pending human-facing text, then a human-lane
153
+ * terminal message when the final adds something. Agent-lane status beats
154
+ * alone still receive a human-lane fallback before the turn closes.
155
155
  */
156
156
  async finish(finalText) {
157
157
  if (this.finished)
@@ -171,42 +171,25 @@ export class ProgressPoster {
171
171
  // Grok's final is every message concatenated, so it repeats and is skipped.
172
172
  // A silent turn (run.ts decides via sentinel/empty) never reaches here.
173
173
  if (this.buffer) {
174
+ const terminalParts = this.said.filter((s) => !containsNoReplySentinel(s));
174
175
  this.pending = [];
175
176
  this.heldQuestions = [];
176
- for (const s of this.said) {
177
- if (!containsNoReplySentinel(s))
178
- this.pending.push(s);
179
- }
180
177
  if (final && !containsNoReplySentinel(final) && !this.repeatsSaid(final)) {
181
- if (this.sendFinalFn) {
182
- // Keep the audience split: collected messages go through send, the
183
- // final reply through sendFinal.
184
- this.flushNow();
185
- this.enqueueWith(final, this.sendFinalFn);
186
- return this.chain;
187
- }
188
- this.pending.push(final);
178
+ terminalParts.push(final);
189
179
  }
190
- this.flushNow();
180
+ this.enqueueToPerson(terminalParts.join("\n\n") || "(no reply)");
191
181
  return this.chain;
192
182
  }
193
183
  const held = this.heldQuestions;
194
184
  this.heldQuestions = [];
195
- const shouldAddFinal = Boolean(final && !this.repeatsSaid(final));
196
- if (!shouldAddFinal && held.length > 0)
197
- this.pending.push(...held);
198
- if (this.pending.length === 0 && this.queued === 0 && !shouldAddFinal)
199
- this.pending.push("(no reply)");
200
- if (this.sendFinalFn && shouldAddFinal) {
201
- // Flush any remaining progress messages first, then the final reply separately.
202
- this.flushNow();
203
- this.enqueueWith(final, this.sendFinalFn);
204
- }
205
- else {
206
- if (shouldAddFinal)
207
- this.pending.push(final);
208
- this.flushNow();
209
- }
185
+ this.flushNow();
186
+ const shouldAddFinal = Boolean(final && !containsNoReplySentinel(final) && !this.repeatsSaid(final));
187
+ if (shouldAddFinal)
188
+ this.enqueueToPerson(final);
189
+ else if (held.length > 0)
190
+ this.enqueueToPerson(held.join("\n\n"));
191
+ else if (this.personQueued === 0)
192
+ this.enqueueToPerson("(no reply)");
210
193
  return this.chain;
211
194
  }
212
195
  /**
@@ -226,9 +209,11 @@ export class ProgressPoster {
226
209
  this.heldQuestions = [];
227
210
  return this.chain;
228
211
  }
229
- this.pending.push(...this.heldQuestions);
212
+ const held = this.heldQuestions;
230
213
  this.heldQuestions = [];
231
214
  this.flushNow();
215
+ if (held.length > 0)
216
+ this.enqueueToPerson(held.join("\n\n"));
232
217
  return this.chain;
233
218
  }
234
219
  repeatsSaid(final) {
@@ -256,7 +241,11 @@ export class ProgressPoster {
256
241
  this.enqueue(body);
257
242
  }
258
243
  enqueue(body) {
259
- this.enqueueWith(body, this.send);
244
+ this.enqueueToPerson(body);
245
+ }
246
+ enqueueToPerson(body) {
247
+ this.personQueued += 1;
248
+ this.enqueueWith(body, this.sendToPerson);
260
249
  }
261
250
  enqueueWith(body, sendFn) {
262
251
  // Defence in depth: never let the silence sentinel reach the chat, whatever
@@ -766,8 +766,8 @@ export async function runBot(deps) {
766
766
  ...deps.progress,
767
767
  ...(args.workingNotice === false || silenceable || isSetupBot ? { workingNoticeMs: 0 } : {}),
768
768
  ...(silenceable || args.suppressOutput ? { buffer: true } : {}),
769
- send: makeSendFn(args.send),
770
- ...(args.sendFinal ? { sendFinal: makeSendFn(args.sendFinal) } : {}),
769
+ sendProgress: makeSendFn(args.sendProgress),
770
+ sendToPerson: makeSendFn(args.sendToPerson),
771
771
  });
772
772
  const controller = new AbortController();
773
773
  currentTurns.add(controller);
@@ -827,7 +827,7 @@ export async function runBot(deps) {
827
827
  const root = rootEventId ? { rootEventId } : {};
828
828
  return target.kind === "dm"
829
829
  ? api.sendDm({ toPersonUid: target.peerUid, body, ...root, ...(audience ? { audience } : {}) })
830
- : api.sendChannelMessage({ channelId: target.channelId, body, ...root });
830
+ : api.sendChannelMessage({ channelId: target.channelId, body, ...root, ...(audience ? { audience } : {}) });
831
831
  };
832
832
  /**
833
833
  * Post `parts` in order, starting at `from`. A failure throws a
@@ -863,15 +863,15 @@ export async function runBot(deps) {
863
863
  throw err;
864
864
  }
865
865
  };
866
- const send = makeSend(target.kind === "dm" ? "agent" : undefined);
867
- const sendFinal = makeSend(target.kind === "dm" ? "human" : undefined);
866
+ const sendProgress = makeSend("agent");
867
+ const sendToPerson = makeSend("human");
868
868
  const saved = (reply) => {
869
869
  const parts = splitBody(reply);
870
870
  const partial = last.value;
871
871
  const sameBody = partial !== null && partial.parts.length === parts.length && partial.parts.every((p, i) => p === parts[i]);
872
872
  return { reply, replyParts: parts, replyPartsDelivered: sameBody ? partial.delivered : 0 };
873
873
  };
874
- return { send, sendFinal, saved };
874
+ return { sendProgress, sendToPerson, saved };
875
875
  };
876
876
  // ── Scheduled jobs ────────────────────────────────────────────────────────
877
877
  /** Newest pending run key already picked up, per jobId (one catch-up per wake). */
@@ -1113,14 +1113,14 @@ export async function runBot(deps) {
1113
1113
  }
1114
1114
  };
1115
1115
  const target = { kind: "dm", peerUid };
1116
- const { send, sendFinal, saved } = trackingSend(target);
1116
+ const { sendProgress, sendToPerson, saved } = trackingSend(target);
1117
1117
  const { outcome, poster } = await runTurnWithProgress({
1118
1118
  marker: { messageId: KICKOFF_MESSAGE_ID, target, ...(thinking && introEventId ? { reactionEventId: introEventId } : {}) },
1119
1119
  prompt,
1120
1120
  sessionScope: "dm",
1121
1121
  tReceived,
1122
- send,
1123
- sendFinal,
1122
+ sendProgress,
1123
+ sendToPerson,
1124
1124
  // The intro already said the bot is starting, and the app shows it thinking.
1125
1125
  workingNotice: false,
1126
1126
  });
@@ -1146,7 +1146,7 @@ export async function runBot(deps) {
1146
1146
  clearInflight(dir, KICKOFF_MESSAGE_ID);
1147
1147
  patchBotConfig(dir, { kickoffPendingSignIn: true });
1148
1148
  markSignInBroken(outcome.excerpt);
1149
- await tellSignInNeeded("dm", send, true);
1149
+ await tellSignInNeeded("dm", sendToPerson, true);
1150
1150
  return;
1151
1151
  }
1152
1152
  if (readBotConfig(dir)?.kickoffPendingSignIn)
@@ -1158,7 +1158,7 @@ export async function runBot(deps) {
1158
1158
  ? `${didNotFinishText(failureReason(outcome), poster.postedCount > 0)} Send me any message and I'll pick up from there.`
1159
1159
  : `Sorry — I couldn't get started on my own (${runtime.id} failed: ${outcome.excerpt.slice(0, 160)}). Send me any message and I'll pick up from there, or check that ${runtime.id} is signed in on this computer.`;
1160
1160
  try {
1161
- await send(body);
1161
+ await sendToPerson(body);
1162
1162
  clearInflight(dir, KICKOFF_MESSAGE_ID);
1163
1163
  }
1164
1164
  catch {
@@ -1235,8 +1235,8 @@ export async function runBot(deps) {
1235
1235
  throw err;
1236
1236
  }
1237
1237
  };
1238
- const send = makeSend(tracking.send);
1239
- const sendFinal = makeSend(tracking.sendFinal);
1238
+ const sendProgress = makeSend(tracking.sendProgress);
1239
+ const sendToPerson = makeSend(tracking.sendToPerson);
1240
1240
  const normalize = (text) => text.replace(/\s+/g, " ").trim();
1241
1241
  /**
1242
1242
  * True only when the final report itself reached the chat in full. With no
@@ -1272,8 +1272,8 @@ export async function runBot(deps) {
1272
1272
  prompt: buildJobTurnPrompt(job, { now: now(), timeZone }),
1273
1273
  sessionScope: target.sessionScope,
1274
1274
  tReceived,
1275
- send,
1276
- sendFinal,
1275
+ sendProgress,
1276
+ sendToPerson,
1277
1277
  // A scheduled run has nobody waiting in a chat: no "working on it" filler.
1278
1278
  workingNotice: false,
1279
1279
  ...(quietJob ? { suppressOutput: true } : {}),
@@ -1307,7 +1307,7 @@ export async function runBot(deps) {
1307
1307
  clearInflight(dir, id);
1308
1308
  attempts.delete(id);
1309
1309
  markSignInBroken(outcome.excerpt);
1310
- await tellSignInNeeded(`job:${jobId}`, (body) => sendToTarget(markerTarget, body));
1310
+ await tellSignInNeeded(`job:${jobId}`, (body) => sendToTarget(markerTarget, body, undefined, "human"));
1311
1311
  log("warn", `kept scheduled run ${id} of job ${jobId} until ${runtime.id} can sign in again`);
1312
1312
  return; // un-acked, un-reported: run once the sign-in works
1313
1313
  }
@@ -1320,7 +1320,7 @@ export async function runBot(deps) {
1320
1320
  return;
1321
1321
  }
1322
1322
  if (attempt >= MAX_ATTEMPTS_PER_MESSAGE) {
1323
- await send(`I couldn't run this one — ${runtime.id} kept failing: ${excerpt.slice(0, 160)}.`).catch(() => undefined);
1323
+ await sendToPerson(`I couldn't run this one — ${runtime.id} kept failing: ${excerpt.slice(0, 160)}.`).catch(() => undefined);
1324
1324
  await completeJobRun(jobId, id, { status: "failed", output: jobRunOutput(excerpt), disposition: `${runtime.id} could not be started` });
1325
1325
  markReported("failed");
1326
1326
  markProcessed(dir, id, now().toISOString());
@@ -1339,7 +1339,7 @@ export async function runBot(deps) {
1339
1339
  // reports and says this real reason, not "the bot was restarted".
1340
1340
  patchInflight(dir, id, { jobFailure: { reason, ...(outcome.excerpt ? { excerpt: outcome.excerpt } : {}) } });
1341
1341
  try {
1342
- await send(didNotFinishText(reason, poster.postedCount > 0));
1342
+ await sendToPerson(didNotFinishText(reason, poster.postedCount > 0));
1343
1343
  }
1344
1344
  catch (err) {
1345
1345
  // Leave it un-acked with the marker: the next poll reports it.
@@ -1534,7 +1534,7 @@ export async function runBot(deps) {
1534
1534
  const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? ownerText :
1535
1535
  `[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
1536
1536
  const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
1537
- const { send, sendFinal, saved } = trackingSend(markerTarget, item.rootEventId);
1537
+ const { sendProgress, sendToPerson, saved } = trackingSend(markerTarget, item.rootEventId);
1538
1538
  const { outcome, poster } = await runTurnWithProgress({
1539
1539
  marker: {
1540
1540
  messageId: id,
@@ -1545,8 +1545,8 @@ export async function runBot(deps) {
1545
1545
  prompt,
1546
1546
  sessionScope,
1547
1547
  tReceived,
1548
- send,
1549
- sendFinal,
1548
+ sendProgress,
1549
+ sendToPerson,
1550
1550
  silenceable: target.kind === "room" && !target.mentioned,
1551
1551
  ...(readDirs.length ? { readDirs } : {}),
1552
1552
  ...(target.kind === "room"
@@ -1605,7 +1605,7 @@ export async function runBot(deps) {
1605
1605
  attempts.delete(id);
1606
1606
  markSignInBroken(outcome.excerpt);
1607
1607
  const conversation = markerTarget.kind === "dm" ? sessionScope : `room:${markerTarget.channelId}:${item.rootEventId ?? ""}`;
1608
- await tellSignInNeeded(conversation, send);
1608
+ await tellSignInNeeded(conversation, sendToPerson);
1609
1609
  log("warn", `kept ${id} until ${runtime.id} can sign in again`);
1610
1610
  return; // un-acked: answered once the sign-in works
1611
1611
  }
@@ -1620,7 +1620,7 @@ export async function runBot(deps) {
1620
1620
  }
1621
1621
  if (attempt >= MAX_ATTEMPTS_PER_MESSAGE) {
1622
1622
  try {
1623
- await send(`Sorry — I couldn't answer that one (${runtime.id} kept failing: ${excerpt.slice(0, 160)}). Try again in a bit, or check that ${runtime.id} is signed in on this computer.`);
1623
+ await sendToPerson(`Sorry — I couldn't answer that one (${runtime.id} kept failing: ${excerpt.slice(0, 160)}). Try again in a bit, or check that ${runtime.id} is signed in on this computer.`);
1624
1624
  }
1625
1625
  catch {
1626
1626
  /* best-effort */
@@ -1660,7 +1660,7 @@ export async function runBot(deps) {
1660
1660
  // still say we did not finish — but with a plain reason, never the raw
1661
1661
  // internal error string.
1662
1662
  const reason = reasoningOnly ? "the model returned no answer" : failureReason(outcome);
1663
- await send(didNotFinishText(reason, poster.postedCount > 0));
1663
+ await sendToPerson(didNotFinishText(reason, poster.postedCount > 0));
1664
1664
  }
1665
1665
  catch (err) {
1666
1666
  // Leave it un-acked with the marker: the next poll says it did not finish.
@@ -26,7 +26,7 @@ export declare const MAX_SENTRY_REPORT_METADATA_VALUE_BYTES = 256;
26
26
  * is owner-approved telemetry for the exact hook script and may include a
27
27
  * company directory; it is never a substitute for hook input or a transcript.
28
28
  */
29
- export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "spawn_ms", "slow_child", "slow_child_ms", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "shell", "nproc", "hook_sequence", "hook_timeout_debug_context"];
29
+ export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "spawn_ms", "slow_child", "slow_child_ms", "slow_phase", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "shell", "nproc", "hook_sequence", "hook_timeout_debug_context"];
30
30
  export declare const MAX_SENTRY_REPORT_METADATA_KEYS: number;
31
31
  export type HookSequenceEntry = {
32
32
  script: string;
@@ -130,18 +130,25 @@ const DEBUG_CONTEXT_HOOK_EVENTS = new Set([
130
130
  "TaskCompleted",
131
131
  "TeammateIdle",
132
132
  ]);
133
- const DEBUG_CONTEXT_PHASES = new Set([
133
+ const SAFE_SLOW_PHASES = new Set([
134
134
  "startup",
135
135
  "source",
136
136
  "config_load",
137
137
  "policy_load",
138
138
  "external_command",
139
139
  "output_write",
140
+ "output_scan",
141
+ "output_merge",
142
+ "output_stdout",
143
+ "output_abandoned_scan",
144
+ "output_abandoned_merge",
145
+ "output_abandoned_stdout",
140
146
  "wait",
141
147
  "child_wait",
142
148
  "probe",
143
149
  "parse",
144
150
  ]);
151
+ const DEBUG_CONTEXT_PHASES = new Set(SAFE_SLOW_PHASES);
145
152
  /**
146
153
  * The only hook diagnostics that can be attached as metadata. The list omits
147
154
  * command text, prompts, payloads, company names, and repositories. `hook_path`
@@ -166,6 +173,7 @@ export const ALLOWED_SENTRY_REPORT_METADATA_KEYS = [
166
173
  "spawn_ms",
167
174
  "slow_child",
168
175
  "slow_child_ms",
176
+ "slow_phase",
169
177
  "exit_code",
170
178
  "final_elapsed_ms",
171
179
  "bash_env_set",
@@ -255,6 +263,11 @@ function boundedDebugName(value, known) {
255
263
  function boundedDebugPhase(value) {
256
264
  return typeof value === "string" && DEBUG_CONTEXT_PHASES.has(value) ? value : "other";
257
265
  }
266
+ function safeSlowPhase(value) {
267
+ if (typeof value !== "string")
268
+ return "unknown";
269
+ return SAFE_SLOW_PHASES.has(value) ? value : "other";
270
+ }
258
271
  function boundedDebugHookEvent(value) {
259
272
  return typeof value === "string" && DEBUG_CONTEXT_HOOK_EVENTS.has(value) ? value : "other";
260
273
  }
@@ -425,6 +438,9 @@ function parseSentryReportEvent(stdin) {
425
438
  if (typeof metadata.slow_child === "string") {
426
439
  metadata.slow_child = safeSlowChild(metadata);
427
440
  }
441
+ if ("slow_phase" in metadata) {
442
+ metadata.slow_phase = safeSlowPhase(metadata.slow_phase);
443
+ }
428
444
  return {
429
445
  code: 0,
430
446
  event: {
@@ -678,6 +694,7 @@ function hookTimeoutTags(event) {
678
694
  load_bucket: loadAverage === "unavailable" ? "unavailable" : loadBucket(loadAverage),
679
695
  spawn_ms_bucket: spawnMsBucket(metadata),
680
696
  slow_child: safeSlowChild(metadata),
697
+ slow_phase: safeSlowPhase(metadata.slow_phase),
681
698
  slow_child_ms_bucket: typeof slowChildMs === "number" && Number.isFinite(slowChildMs)
682
699
  ? elapsedBucket(slowChildMs)
683
700
  : "unknown",
@@ -0,0 +1,9 @@
1
+ interface DaemonCallDependencies {
2
+ home: string;
3
+ env: NodeJS.ProcessEnv;
4
+ out: (line: string) => void;
5
+ }
6
+ /** Send one CLI request after its protocol module has been preloaded. */
7
+ export declare function runDaemonCall(op: string, args: Record<string, unknown>, timeoutMs: number, deps: DaemonCallDependencies): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=command-call.d.ts.map
@@ -0,0 +1,18 @@
1
+ /** Send one CLI request after its protocol module has been preloaded. */
2
+ export async function runDaemonCall(op, args, timeoutMs, deps) {
3
+ const { hqdRequest, hqdSocketPath, HqdClientError } = await import("./protocol.js");
4
+ try {
5
+ const result = await hqdRequest(op, args, {
6
+ socketPath: hqdSocketPath(deps.home, deps.env),
7
+ timeoutMs,
8
+ });
9
+ deps.out(JSON.stringify(result, null, 2));
10
+ }
11
+ catch (error) {
12
+ if (!(error instanceof HqdClientError))
13
+ throw error;
14
+ deps.out(JSON.stringify({ error: { code: error.code, message: error.message } }, null, 2));
15
+ process.exitCode = 1;
16
+ }
17
+ }
18
+ //# sourceMappingURL=command-call.js.map
@@ -13,6 +13,7 @@ import { workContextRoot, workContextSessionsDir } from "../../work-context/path
13
13
  import { countDistinctHeldEvents, countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
14
14
  import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
15
15
  import { collectDaemonDoctor, formatDaemonDoctor, } from "../../mesh/live/daemon/doctor.js";
16
+ import { workSessionDenialReason } from "../../mesh/denial-codes.js";
16
17
  export const WORK_CONTEXT_FAMILY_ID = "work-context";
17
18
  export const WORK_CONTEXT_FAMILY_TITLE = "Work context";
18
19
  const DEFAULT_DEPS = {
@@ -97,6 +98,9 @@ function runChecks(deps) {
97
98
  ]
98
99
  .filter(Boolean)
99
100
  .join(" ");
101
+ const quarantineDenialReason = showQuarantineReason
102
+ ? workSessionDenialReason(stats.lastQuarantineServerCode)
103
+ : undefined;
100
104
  results.push({
101
105
  status: stats.quarantined > 0 ? "WARN" : "PASS",
102
106
  checkId: "work-context.outbox-quarantined",
@@ -104,6 +108,7 @@ function runChecks(deps) {
104
108
  message: `Outbox quarantined count=${stats.quarantined}${showQuarantineReason
105
109
  ? ` (last reason=${stats.lastQuarantineReason ?? "unknown"}${quarantineDetails ? `; ${quarantineDetails}` : ""})`
106
110
  : ""}.`,
111
+ ...(quarantineDenialReason ? { remediation: quarantineDenialReason } : {}),
107
112
  });
108
113
  // Default-company mode + migration capability (US-017B)
109
114
  try {
@@ -146,6 +146,7 @@ export declare function dispatchOutpostPickup(port: OutpostExecPort, input: {
146
146
  command: string;
147
147
  timeoutSeconds?: number;
148
148
  waitMs?: number;
149
+ beforeWait?: () => Promise<void>;
149
150
  }): Promise<OutpostDispatchResult>;
150
151
  export declare function dispatchErrorMessage(err: unknown): string;
151
152
  /** Factory matching `hq outposts exec`'s client wiring. */
@@ -252,6 +252,7 @@ export async function stageOutpostPayload(port, outpostId, bytes, put = fetch) {
252
252
  }
253
253
  export async function dispatchOutpostPickup(port, input) {
254
254
  const submitted = await port.submitExec(input.command, input.outpostId, input.timeoutSeconds ?? PICKUP_EXEC_TIMEOUT_SECONDS);
255
+ await input.beforeWait?.();
255
256
  const ac = new AbortController();
256
257
  const waitMs = input.waitMs ?? PICKUP_WAIT_MS;
257
258
  const timer = setTimeout(() => ac.abort(), waitMs);
@@ -79,6 +79,17 @@ export function createWorkSessionDeliverer(opts) {
79
79
  if (status === 429 || (status >= 500 && status <= 599)) {
80
80
  return { ok: false, retryable: true, code: `HTTP_${status}` };
81
81
  }
82
+ if (status === 404 && isWorkSessionDenialCode(errorFields.code)) {
83
+ // Keep the allowlisted code (FEATURE_DISABLED) so the refusal can be
84
+ // explained and picked up by the default requeue filter.
85
+ return {
86
+ ok: false,
87
+ retryable: false,
88
+ code: "HTTP_404",
89
+ httpStatus: 404,
90
+ serverCode: errorFields.code,
91
+ };
92
+ }
82
93
  if (status === 404 ||
83
94
  status === 409 ||
84
95
  status === 410 ||
@@ -1,5 +1,16 @@
1
- /** Explicit 401/403 codes returned by hq-pro's work-session registration path. */
2
- export declare const WORK_SESSION_DENIAL_CODES: readonly ["AUTH_REQUIRED", "AGENT_IDENTITY_UNVERIFIED", "IMPERSONATION_FORBIDDEN", "NO_PERSON_ENTITY", "INVALID_COMPANY", "WORK_REGISTRY_DISABLED"];
1
+ /**
2
+ * Explicit refusal codes returned by hq-pro's work-session registration path:
3
+ * the 401/403 denials, plus the 404 FEATURE_DISABLED answer given while the
4
+ * company's Work Mesh Live setting is off.
5
+ */
6
+ export declare const WORK_SESSION_DENIAL_CODES: readonly ["AUTH_REQUIRED", "AGENT_IDENTITY_UNVERIFIED", "IMPERSONATION_FORBIDDEN", "NO_PERSON_ENTITY", "INVALID_COMPANY", "WORK_REGISTRY_DISABLED", "FEATURE_DISABLED"];
7
+ /**
8
+ * Refusals that end once the company owner turns a setting on. The default
9
+ * `hq mesh context requeue` filter re-sends operations stored with these.
10
+ */
11
+ export declare const WORK_TRACKING_OFF_DENIAL_CODES: readonly WorkSessionDenialCode[];
3
12
  export type WorkSessionDenialCode = (typeof WORK_SESSION_DENIAL_CODES)[number];
4
13
  export declare function isWorkSessionDenialCode(value: unknown): value is WorkSessionDenialCode;
14
+ /** Plain reason for a server denial code, or undefined when there is none. */
15
+ export declare function workSessionDenialReason(value: unknown): string | undefined;
5
16
  //# sourceMappingURL=denial-codes.d.ts.map
@@ -1,4 +1,8 @@
1
- /** Explicit 401/403 codes returned by hq-pro's work-session registration path. */
1
+ /**
2
+ * Explicit refusal codes returned by hq-pro's work-session registration path:
3
+ * the 401/403 denials, plus the 404 FEATURE_DISABLED answer given while the
4
+ * company's Work Mesh Live setting is off.
5
+ */
2
6
  export const WORK_SESSION_DENIAL_CODES = [
3
7
  "AUTH_REQUIRED",
4
8
  "AGENT_IDENTITY_UNVERIFIED",
@@ -6,9 +10,29 @@ export const WORK_SESSION_DENIAL_CODES = [
6
10
  "NO_PERSON_ENTITY",
7
11
  "INVALID_COMPANY",
8
12
  "WORK_REGISTRY_DISABLED",
13
+ "FEATURE_DISABLED",
14
+ ];
15
+ /**
16
+ * Refusals that end once the company owner turns a setting on. The default
17
+ * `hq mesh context requeue` filter re-sends operations stored with these.
18
+ */
19
+ export const WORK_TRACKING_OFF_DENIAL_CODES = [
20
+ "WORK_REGISTRY_DISABLED",
21
+ "FEATURE_DISABLED",
9
22
  ];
10
23
  export function isWorkSessionDenialCode(value) {
11
24
  return typeof value === "string" &&
12
25
  WORK_SESSION_DENIAL_CODES.includes(value);
13
26
  }
27
+ /** Plain-language reasons for denial codes the user can act on. */
28
+ const WORK_SESSION_DENIAL_REASONS = {
29
+ WORK_REGISTRY_DISABLED: "Work tracking is turned off for this company. The company owner can turn it on with: hq company --company <slug> settings set --work-registry-enabled true",
30
+ FEATURE_DISABLED: "Live work tracking is turned off for this company. The company owner can turn it on with: hq company --company <slug> settings set --work-mesh-live-enabled true",
31
+ };
32
+ /** Plain reason for a server denial code, or undefined when there is none. */
33
+ export function workSessionDenialReason(value) {
34
+ return isWorkSessionDenialCode(value)
35
+ ? WORK_SESSION_DENIAL_REASONS[value]
36
+ : undefined;
37
+ }
14
38
  //# sourceMappingURL=denial-codes.js.map
@@ -23,6 +23,7 @@ import { readDaemonState } from "./state.js";
23
23
  import { CLI_VERSION } from "../../../../cli-version.js";
24
24
  import { resolveRunningCliEntry } from "./install.js";
25
25
  import { readInstalledCliVersion } from "./version-drift.js";
26
+ import { workSessionDenialReason } from "../../denial-codes.js";
26
27
  import { LEGACY_LISTENER_DOCTOR_LINE, legacyListenInstalled, } from "./legacy-listen.js";
27
28
  export const UNHEALTHY_SPOOL_AGE_MS = 60_000;
28
29
  function oldestJsonlAgeMs(filePath, nowMs) {
@@ -321,6 +322,9 @@ export function formatDaemonDoctor(report) {
321
322
  .filter(Boolean)
322
323
  .join(" ");
323
324
  lines.push(`outbox quarantined: ${report.outboxQuarantined} (last reason=${report.outboxLastQuarantineReason ?? "unknown"}${quarantineDetails ? `; ${quarantineDetails}` : ""})`);
325
+ const denialReason = workSessionDenialReason(report.outboxLastQuarantineServerCode);
326
+ if (denialReason)
327
+ lines.push(denialReason);
324
328
  }
325
329
  if (report.emitMode !== "legacy" && report.heldCount > 0) {
326
330
  lines.push(`held: ${report.heldCount} (needs_company; not sent as NONE)`);
@@ -8,7 +8,7 @@ import * as path from "node:path";
8
8
  import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
9
9
  import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
10
10
  import { NotTrackingError } from "./errors.js";
11
- import { isWorkSessionDenialCode } from "../mesh/denial-codes.js";
11
+ import { isWorkSessionDenialCode, WORK_TRACKING_OFF_DENIAL_CODES, } from "../mesh/denial-codes.js";
12
12
  import { readSessionState, stripRegisterReceiptEpisodes, updateSessionState, writeAcknowledgedRegisterBinding, } from "./state.js";
13
13
  import { isSafeWorkContextSegment, workContextOutboxDir, workContextOutboxPath, } from "./paths.js";
14
14
  /** Base delay for outbox retry backoff (attempt 1 → 30s). */
@@ -390,8 +390,14 @@ export function requeueQuarantinedOutbox(root, opts = {}) {
390
390
  for (const op of listOutboxOperations(root)) {
391
391
  if (op.delivery !== "quarantined")
392
392
  continue;
393
- if (errorCode !== null && op.lastErrorCode !== errorCode)
393
+ // Default filter: AUTH_DENIED, plus refusals stored with a server code
394
+ // that means work tracking was off for the company (a 404 FEATURE_DISABLED
395
+ // is stored as HTTP_404). An explicit errorCode matches lastErrorCode only.
396
+ const workTrackingWasOff = opts.errorCode === undefined &&
397
+ WORK_TRACKING_OFF_DENIAL_CODES.includes(op.lastServerErrorCode ?? "");
398
+ if (errorCode !== null && op.lastErrorCode !== errorCode && !workTrackingWasOff) {
394
399
  continue;
400
+ }
395
401
  op.delivery = "queued";
396
402
  op.attemptCount = 0;
397
403
  op.lastErrorCode = undefined;
@@ -186,7 +186,11 @@ export async function emitCliFeatureUsed(usage, timeoutMs = TELEMETRY_TIMEOUT_MS
186
186
  consentBasis,
187
187
  schemaVersion: 1,
188
188
  ...(companyUid ? { companyUid } : {}),
189
- properties: { kind: usage.kind, outcome: usage.outcome },
189
+ properties: {
190
+ kind: usage.kind,
191
+ outcome: usage.outcome,
192
+ version: CLI_VERSION.slice(0, 40),
193
+ },
190
194
  },
191
195
  ],
192
196
  },
@@ -278,7 +278,8 @@ type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "v
278
278
  /**
279
279
  * Poll until the install tree has been continuously READY for `quietMs`, or the
280
280
  * wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
281
- * pid, (ii) the missing target is present, and — when `packageRoot` is known —
281
+ * pid and no live recorded updater while the tree is in flight, (ii) the missing
282
+ * target is present, and — when `packageRoot` is known —
282
283
  * (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and (iv) no
283
284
  * `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
284
285
  * its condition "not ready" rather than throwing.
@@ -295,20 +296,22 @@ type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "v
295
296
  * any installer before attempting a repair. Any observed signature change
296
297
  * keeps the ordinary settle deadline in force. `deadlineMs === 0` evaluates
297
298
  * readiness once.
298
- * - LOCKED time — wall time observed under a fresh foreign lock held by a live
299
- * pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
300
- * 10-minute staleness window) and does NOT consume the settle budget. When
301
- * that ceiling is reached with the lock still held, the result is unsettled
302
- * with `lockHeldAtEnd` true so the seam can report a distinct `lock-held`
303
- * outcome and remedy rather than telling the user to reinstall over a live
304
- * writer.
305
- * Each poll's REAL elapsed interval (now() deltas) is attributed to the locked or
306
- * unlocked bucket by whether a fresh foreign lock was held when the interval
307
- * began, so a starved event loop only makes the wait coarser, never unbounded. A
299
+ * - WRITER time — wall time observed under a fresh foreign lock, or while a
300
+ * recorded foreign updater pid is alive and the tree has an in-flight signal
301
+ * — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
302
+ * 10-minute staleness window) and does NOT consume the settle budget. This
303
+ * also covers a still-running updater whose lock became stale by age. When
304
+ * that ceiling is reached, the result is unsettled; `lockHeldAtEnd` remains
305
+ * reserved for a fresh lock held at the end.
306
+ * Each poll's REAL elapsed interval (now() deltas) is attributed to the writer or
307
+ * unlocked bucket by whether a fresh lock or qualifying live updater was observed
308
+ * when the interval began, so a starved event loop only makes the wait coarser,
309
+ * never unbounded. A
308
310
  * stale lock (dead pid, older than the staleness window, unparseable) or our own
309
- * pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
311
+ * pid never extends anything by itself. The whole wait is bounded (≤ `deadlineMs +
310
312
  * lockWaitMs`), and the unchanged-tree path normally returns after the much
311
- * shorter `staleProbeMs` observation window.
313
+ * shorter `staleProbeMs` observation window unless a live updater pid is still
314
+ * present with an in-flight tree signal.
312
315
  */
313
316
  export declare function waitForInstallTreeSettled(args: WaitForInstallTreeSettledArgs): Promise<WaitForInstallTreeSettledResult>;
314
317
  export interface InstallRewriteSignalArgs {