@indigoai-us/hq-cli 5.345.93 → 5.345.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.95] — 2026-10-12
6
+ - `hq doctor --fix` still refreshes a stale personal-files journal and a
7
+ company journal in the same pass when hook files have uncommitted changes.
8
+ Session-start health no longer treats that pair as unrepaired.
9
+
10
+ ## [5.345.94] — 2026-10-12
11
+ - Local bots now turn `@Name` in a group-room post into a real mention, so the addressed bot receives that post in its inbox. A room post that does not mention another bot still does not wake that bot.
12
+
5
13
  ## [5.345.93] — 2026-10-12
6
14
  - New `hq capacity` command shows how much of each signed-in Claude, Codex,
7
15
  and Grok account's usage limit is left and when it resets. The numbers come
@@ -249,12 +249,33 @@ export declare class BotApi {
249
249
  * (someone else does). Anything else throws.
250
250
  */
251
251
  claimChannelUnit(channelId: string, unitId: string): Promise<boolean>;
252
+ /**
253
+ * GET /v1/notify/channels/{id}/members — roster used to resolve `@Name`
254
+ * into structured mentions on a room post. Agents may read members of a
255
+ * channel they belong to (same gate as reading messages).
256
+ */
257
+ fetchChannelMembers(channelId: string): Promise<Array<{
258
+ personUid: string;
259
+ displayName?: string;
260
+ email?: string;
261
+ slug?: string;
262
+ handle?: string;
263
+ }>>;
252
264
  /** POST /v1/notify/channels/{id}/messages — post in a room as the bot. */
253
265
  sendChannelMessage(input: {
254
266
  channelId: string;
255
267
  body: string;
256
268
  rootEventId?: string;
257
269
  audience?: "human" | "agent" | "both";
270
+ /**
271
+ * Structured HQ mentions. The server never parses `@Name` from the body;
272
+ * without this array the addressed bot is not written to the agent inbox.
273
+ */
274
+ mentions?: ReadonlyArray<{
275
+ participantUid: string;
276
+ participantType: "agent" | "human";
277
+ displayName: string;
278
+ }>;
258
279
  }): Promise<{
259
280
  eventId?: string;
260
281
  createdAt?: string;
@@ -192,6 +192,30 @@ export class BotApi {
192
192
  throw err;
193
193
  }
194
194
  }
195
+ /**
196
+ * GET /v1/notify/channels/{id}/members — roster used to resolve `@Name`
197
+ * into structured mentions on a room post. Agents may read members of a
198
+ * channel they belong to (same gate as reading messages).
199
+ */
200
+ async fetchChannelMembers(channelId) {
201
+ const { body } = await this.call(`/v1/notify/channels/${encodeURIComponent(channelId)}/members`);
202
+ const rows = Array.isArray(body.members) ? body.members : [];
203
+ const out = [];
204
+ for (const row of rows) {
205
+ if (typeof row.personUid !== "string" || !row.personUid.trim())
206
+ continue;
207
+ out.push({
208
+ personUid: row.personUid.trim(),
209
+ ...(typeof row.displayName === "string" && row.displayName.trim()
210
+ ? { displayName: row.displayName.trim() }
211
+ : {}),
212
+ ...(typeof row.email === "string" && row.email.trim() ? { email: row.email.trim() } : {}),
213
+ ...(typeof row.slug === "string" && row.slug.trim() ? { slug: row.slug.trim() } : {}),
214
+ ...(typeof row.handle === "string" && row.handle.trim() ? { handle: row.handle.trim() } : {}),
215
+ });
216
+ }
217
+ return out;
218
+ }
195
219
  /** POST /v1/notify/channels/{id}/messages — post in a room as the bot. */
196
220
  async sendChannelMessage(input) {
197
221
  const payload = { body: input.body };
@@ -199,6 +223,8 @@ export class BotApi {
199
223
  payload.rootEventId = input.rootEventId;
200
224
  if (input.audience)
201
225
  payload.audience = input.audience;
226
+ if (input.mentions && input.mentions.length > 0)
227
+ payload.mentions = input.mentions;
202
228
  const { body } = await this.call(`/v1/notify/channels/${encodeURIComponent(input.channelId)}/messages`, { method: "POST", body: payload });
203
229
  return body;
204
230
  }
@@ -382,7 +382,8 @@ export function buildSystemPrompt(input) {
382
382
  `- \`agent\`: status pings, tool handoffs, and in-progress notes that only another agent needs to read.\n` +
383
383
  `- \`human\`: anything a person must read — answers, results, questions, summaries.\n` +
384
384
  `- \`both\`: when the content is relevant to both a person and a downstream agent.\n` +
385
- `When unsure, use \`human\`. Progress messages sent before the final answer should use \`agent\`; the final answer should use \`human\`.`);
385
+ `When unsure, use \`human\`. Progress messages sent before the final answer should use \`agent\`; the final answer should use \`human\`.\n\n` +
386
+ `In a group room, write \`@Name\` to address another person or bot on the roster. That becomes a real mention and puts the post in the addressed bot's inbox. A room post that does not mention another bot does not wake that bot.`);
386
387
  return `${parts.join("\n\n")}\n`;
387
388
  }
388
389
  /**
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Best-effort structured mentions for a local bot's room post.
3
+ *
4
+ * HQ Rooms mentions are structured-only: the server never parses `@Name` out of
5
+ * the body. `hq dm` already resolves names against the roster and sends the
6
+ * `mentions` array. Bot room posts did not, so an addressed bot never got an
7
+ * inbox item (feedback #2774). Unknown or ambiguous `@` words are skipped so a
8
+ * model typo cannot swallow the visible reply; matched roster members become
9
+ * real mentions. The sender is never included.
10
+ */
11
+ import { type ChannelMember } from "../../commands/dm.js";
12
+ export type StructuredChannelMention = {
13
+ participantUid: string;
14
+ participantType: "agent" | "human";
15
+ displayName: string;
16
+ };
17
+ export declare function roomPostHasMentionTokens(body: string): boolean;
18
+ export declare function resolveRoomMentionsForBot(args: {
19
+ body: string;
20
+ members: readonly ChannelMember[];
21
+ selfUid: string;
22
+ }): {
23
+ body: string;
24
+ mentions: StructuredChannelMention[];
25
+ };
26
+ //# sourceMappingURL=room-mentions.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Best-effort structured mentions for a local bot's room post.
3
+ *
4
+ * HQ Rooms mentions are structured-only: the server never parses `@Name` out of
5
+ * the body. `hq dm` already resolves names against the roster and sends the
6
+ * `mentions` array. Bot room posts did not, so an addressed bot never got an
7
+ * inbox item (feedback #2774). Unknown or ambiguous `@` words are skipped so a
8
+ * model typo cannot swallow the visible reply; matched roster members become
9
+ * real mentions. The sender is never included.
10
+ */
11
+ import { parseMentionTokens, resolveMentions, stripMentionQuotes, } from "../../commands/dm.js";
12
+ export function roomPostHasMentionTokens(body) {
13
+ return parseMentionTokens(body).length > 0;
14
+ }
15
+ export function resolveRoomMentionsForBot(args) {
16
+ const tokens = parseMentionTokens(args.body);
17
+ if (tokens.length === 0)
18
+ return { body: args.body, mentions: [] };
19
+ const members = [...args.members];
20
+ const mentions = [];
21
+ const claimed = new Set();
22
+ for (const token of tokens) {
23
+ const resolved = resolveMentions([token], members);
24
+ if (!resolved.ok)
25
+ continue;
26
+ for (const mention of resolved.mentions) {
27
+ if (mention.participantUid === args.selfUid)
28
+ continue;
29
+ if (claimed.has(mention.participantUid))
30
+ continue;
31
+ claimed.add(mention.participantUid);
32
+ mentions.push(mention);
33
+ }
34
+ }
35
+ return {
36
+ body: mentions.length > 0 ? stripMentionQuotes(args.body) : args.body,
37
+ mentions,
38
+ };
39
+ }
40
+ //# sourceMappingURL=room-mentions.js.map
@@ -73,6 +73,7 @@ import { appendDmHistory, dmHistoryHasModelThread, markDmHistoryModelTurn, readD
73
73
  import { attachmentTurnBlock, downloadTurnAttachments } from "./attachments.js";
74
74
  import { inboxEntryAttachments } from "../agent-kit/inbox-state.js";
75
75
  import { isAgentSender, isDmItem, isMetaNonReply, resolveReplyTarget } from "./room-policy.js";
76
+ import { resolveRoomMentionsForBot, roomPostHasMentionTokens, } from "./room-mentions.js";
76
77
  import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOutput, machineTimeZone, planJobRuns, stripJobHeader, SUPERSEDED_DISPOSITION } from "./job-run.js";
77
78
  import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
78
79
  import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
@@ -755,6 +756,15 @@ export async function runBot(deps) {
755
756
  }
756
757
  return `${header}\n${recent}${from}: ${text}`;
757
758
  };
759
+ const channelMemberCache = new Map();
760
+ const loadChannelMembers = async (channelId) => {
761
+ const cached = channelMemberCache.get(channelId);
762
+ if (cached)
763
+ return cached;
764
+ const members = await api.fetchChannelMembers(channelId);
765
+ channelMemberCache.set(channelId, members);
766
+ return members;
767
+ };
758
768
  const rememberDm = (scope, message) => {
759
769
  if (!isDmSessionScope(scope))
760
770
  return;
@@ -1052,13 +1062,16 @@ export async function runBot(deps) {
1052
1062
  }
1053
1063
  await sendToTarget(target, notice.text, rootEventId, "human");
1054
1064
  };
1055
- const postOne = (target, body, rootEventId, audience) => {
1065
+ const postOne = async (target, body, rootEventId, audience) => {
1056
1066
  const root = rootEventId ? { rootEventId } : {};
1057
- const send = target.kind === "dm"
1058
- ? api.sendDm({ toPersonUid: target.peerUid, body, ...root, ...(audience ? { audience } : {}) })
1059
- : api.sendChannelMessage({ channelId: target.channelId, body, ...root, ...(audience ? { audience } : {}) });
1060
- return send.then((posted) => {
1061
- if (target.kind === "dm" && audience !== "agent") {
1067
+ if (target.kind === "dm") {
1068
+ const posted = await api.sendDm({
1069
+ toPersonUid: target.peerUid,
1070
+ body,
1071
+ ...root,
1072
+ ...(audience ? { audience } : {}),
1073
+ });
1074
+ if (audience !== "agent") {
1062
1075
  const eventId = posted && typeof posted === "object" && "eventId" in posted && typeof posted.eventId === "string"
1063
1076
  ? posted.eventId
1064
1077
  : undefined;
@@ -1072,6 +1085,31 @@ export async function runBot(deps) {
1072
1085
  });
1073
1086
  }
1074
1087
  return posted;
1088
+ }
1089
+ let bodyToSend = body;
1090
+ let mentions;
1091
+ if (roomPostHasMentionTokens(body)) {
1092
+ try {
1093
+ const members = await loadChannelMembers(target.channelId);
1094
+ const resolved = resolveRoomMentionsForBot({
1095
+ body,
1096
+ members,
1097
+ selfUid: config.agentUid,
1098
+ });
1099
+ bodyToSend = resolved.body;
1100
+ if (resolved.mentions.length > 0)
1101
+ mentions = resolved.mentions;
1102
+ }
1103
+ catch (err) {
1104
+ log("warn", `room mentions for ${target.channelId} unresolved: ${err instanceof Error ? err.message : String(err)}`);
1105
+ }
1106
+ }
1107
+ return api.sendChannelMessage({
1108
+ channelId: target.channelId,
1109
+ body: bodyToSend,
1110
+ ...root,
1111
+ ...(audience ? { audience } : {}),
1112
+ ...(mentions ? { mentions } : {}),
1075
1113
  });
1076
1114
  };
1077
1115
  /**
@@ -8,9 +8,11 @@
8
8
  * `.claude/`, `.codex/`, or `.grok/`, `--fix` refuses hook-file repairs
9
9
  * and exits non-zero unless `--force`, so those writes can never be
10
10
  * tangled up with unrelated in-flight edits to the security layer.
11
- * Repairs that do not write those trees (a stale-company `refresh-sync`
12
- * pull) still run: the SessionStart health hook never passes `--force`,
13
- * and blocking the pull left `sync.journal.<slug>` WARN after `--fix`.
11
+ * Repairs that do not write those trees (`refresh-sync` pulls for a
12
+ * stale company journal or the personal vault) still run: the
13
+ * SessionStart health hook never passes `--force`, and blocking those
14
+ * pulls left `sync.journal.<slug>` and
15
+ * `sync.journal.__hq_personal_vault__` WARN after `--fix`.
14
16
  * 2. Allowlist only. It repairs the classified safe classes — restore an
15
17
  * execute bit, add a hook id to the gate profiles it is missing from,
16
18
  * re-register a script present on disk, or pull a company or personal
@@ -8,9 +8,11 @@
8
8
  * `.claude/`, `.codex/`, or `.grok/`, `--fix` refuses hook-file repairs
9
9
  * and exits non-zero unless `--force`, so those writes can never be
10
10
  * tangled up with unrelated in-flight edits to the security layer.
11
- * Repairs that do not write those trees (a stale-company `refresh-sync`
12
- * pull) still run: the SessionStart health hook never passes `--force`,
13
- * and blocking the pull left `sync.journal.<slug>` WARN after `--fix`.
11
+ * Repairs that do not write those trees (`refresh-sync` pulls for a
12
+ * stale company journal or the personal vault) still run: the
13
+ * SessionStart health hook never passes `--force`, and blocking those
14
+ * pulls left `sync.journal.<slug>` and
15
+ * `sync.journal.__hq_personal_vault__` WARN after `--fix`.
14
16
  * 2. Allowlist only. It repairs the classified safe classes — restore an
15
17
  * execute bit, add a hook id to the gate profiles it is missing from,
16
18
  * re-register a script present on disk, or pull a company or personal
@@ -120,7 +122,10 @@ export async function applyFixes(options) {
120
122
  const refuseDirtyHookWrites = (plans) => {
121
123
  if (dirty.length === 0)
122
124
  return plans;
123
- const allowed = plans.filter((plan) => plan.relpath.length === 0);
125
+ // Journal refreshes never write hook files. Allow them by class, not
126
+ // only by empty relpath, so a personal-vault pull and a company pull
127
+ // still run when SessionStart `--fix --yes` cannot pass `--force`.
128
+ const allowed = plans.filter((plan) => plan.fixClass === "refresh-sync" || plan.relpath.length === 0);
124
129
  if (allowed.length === plans.length)
125
130
  return plans;
126
131
  if (allowed.length === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.93",
3
+ "version": "5.345.95",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {