@indigoai-us/hq-cli 5.118.1 → 5.119.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.119.0] — 2026-09-17
6
+
7
+ ### Changed
8
+
9
+ - External agents no longer copy the company vault onto their own machine.
10
+ `hq agent kit install` used to install a sync loop that mirrored the whole
11
+ vault to disk; a bot reads and writes vault files on demand through the `hq`
12
+ MCP server and the `hq files` commands, so the copy bought nothing for the
13
+ common case and cost a full download of the company's assets. On a team with
14
+ a large brand library that is hundreds of megabytes, and on a sandboxed bot
15
+ host whose egress truncates large responses it could never finish at all —
16
+ every file retried three times and failed with `ERR_HQ_TRUNCATED_RESPONSE`,
17
+ which left the probe stuck and the agent reporting a sync error while it was
18
+ otherwise a healthy team member.
19
+
20
+ Pass `--sync-files` if the bot genuinely needs files on disk for shell-based
21
+ work; a reinstall without it removes a sync loop an earlier install left
22
+ behind. An existing install is unchanged until it is reinstalled.
23
+
24
+ - `hq agent probe` reports a `files` check in place of `team-sync`. Without a
25
+ local copy it proves the read path the bot actually uses — a vault listing
26
+ answers — instead of downloading the vault to prove a folder exists. With
27
+ `--sync-files` it checks the pull exactly as before.
28
+
29
+ - The heartbeat reports only the components the kit runs. A component that was
30
+ never installed is omitted rather than reported as an error, so a bot with no
31
+ sync loop no longer looks broken.
32
+
33
+ - `hq_search` no longer runs a local index search on an agent that has no local
34
+ copy, where it could only ever return nothing. It runs the server-side vault
35
+ search instead, and says plainly that content and semantic search need a kit
36
+ installed with `--sync-files`.
37
+
38
+ ## [5.118.2] — 2026-09-17
39
+
40
+ ### Changed
41
+
42
+ - The locked-workspace notice now says what you can still do. A locked
43
+ workspace is read-only for new things, but deleting still works — which
44
+ matters, because every fix the notice lists (remove members, disconnect
45
+ integrations, delete secrets, deprovision agents) is a delete. The notice
46
+ states that outright instead of leaving "read-only" to be guessed at.
47
+
48
+ - The plan-limit box names the plan: "HQ Starter plan limit exceeded" rather
49
+ than "HQ plan limit exceeded".
50
+
51
+ ### Added
52
+
53
+ - `hq agents provision --model` now sets the brain model during creation. A
54
+ model such as `gpt-5.5` or `grok-4.6` selects the agents-v2 runtime unless
55
+ `--provider` is specified, avoiding a later box recreation to change brains.
56
+
5
57
  ## [5.118.1] — 2026-09-17
6
58
 
7
59
  ### Fixed
@@ -3812,7 +3812,10 @@ export declare const COMMAND_CATALOG: readonly [{
3812
3812
  readonly description: "Agent slug (defaults to a slug of <name>)";
3813
3813
  }, {
3814
3814
  readonly flags: "--provider <provider>";
3815
- readonly description: "Runtime: codex | grok | claude | agents-v2 (default codex). claude is subscription-only; agents-v2 boots its brain box (codex, or grok via a grok-* model) and receives the v2 runtime post-boot";
3815
+ readonly description: "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default";
3816
+ }, {
3817
+ readonly flags: "--model <model>";
3818
+ readonly description: "Brain model, for example gpt-5.5 or grok-4.6";
3816
3819
  }, {
3817
3820
  readonly flags: "--auth-mode <mode>";
3818
3821
  readonly description: "Auth: subscription | apiKey (default subscription)";
@@ -4178,7 +4181,7 @@ export declare const COMMAND_CATALOG: readonly [{
4178
4181
  readonly options: readonly [];
4179
4182
  readonly subcommands: readonly [{
4180
4183
  readonly name: "install";
4181
- readonly description: "Install sync, work-mesh, inbox and heartbeat services plus the skills directory";
4184
+ readonly description: "Install the work-mesh, inbox and heartbeat services plus the skills directory";
4182
4185
  readonly aliases: readonly [];
4183
4186
  readonly hidden: false;
4184
4187
  readonly usage: "[options]";
@@ -4186,6 +4189,12 @@ export declare const COMMAND_CATALOG: readonly [{
4186
4189
  readonly options: readonly [{
4187
4190
  readonly flags: "--hq-root <path>";
4188
4191
  readonly description: "Local HQ tree to sync the company vault into (default: ~/.hq-agent/hq)";
4192
+ }, {
4193
+ readonly flags: "--sync-files";
4194
+ readonly description: "Also mirror the company vault to disk. Off by default: files are read on demand through the hq MCP server";
4195
+ }, {
4196
+ readonly flags: "--no-sync-files";
4197
+ readonly description: "Do not mirror the vault to disk (default; removes the loop if an earlier install added it)";
4189
4198
  }, {
4190
4199
  readonly flags: "--inbox-ack";
4191
4200
  readonly description: "Ack mirrored inbox items on the server (default: leave acking to the bot)";
@@ -4926,7 +4926,11 @@ export const COMMAND_CATALOG = [
4926
4926
  },
4927
4927
  {
4928
4928
  "flags": "--provider <provider>",
4929
- "description": "Runtime: codex | grok | claude | agents-v2 (default codex). claude is subscription-only; agents-v2 boots its brain box (codex, or grok via a grok-* model) and receives the v2 runtime post-boot"
4929
+ "description": "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default"
4930
+ },
4931
+ {
4932
+ "flags": "--model <model>",
4933
+ "description": "Brain model, for example gpt-5.5 or grok-4.6"
4930
4934
  },
4931
4935
  {
4932
4936
  "flags": "--auth-mode <mode>",
@@ -5406,7 +5410,7 @@ export const COMMAND_CATALOG = [
5406
5410
  "subcommands": [
5407
5411
  {
5408
5412
  "name": "install",
5409
- "description": "Install sync, work-mesh, inbox and heartbeat services plus the skills directory",
5413
+ "description": "Install the work-mesh, inbox and heartbeat services plus the skills directory",
5410
5414
  "aliases": [],
5411
5415
  "hidden": false,
5412
5416
  "usage": "[options]",
@@ -5416,6 +5420,14 @@ export const COMMAND_CATALOG = [
5416
5420
  "flags": "--hq-root <path>",
5417
5421
  "description": "Local HQ tree to sync the company vault into (default: ~/.hq-agent/hq)"
5418
5422
  },
5423
+ {
5424
+ "flags": "--sync-files",
5425
+ "description": "Also mirror the company vault to disk. Off by default: files are read on demand through the hq MCP server"
5426
+ },
5427
+ {
5428
+ "flags": "--no-sync-files",
5429
+ "description": "Do not mirror the vault to disk (default; removes the loop if an earlier install added it)"
5430
+ },
5419
5431
  {
5420
5432
  "flags": "--inbox-ack",
5421
5433
  "description": "Ack mirrored inbox items on the server (default: leave acking to the bot)"
@@ -35,6 +35,13 @@ export interface KitInstallOptions {
35
35
  hqRoot?: string;
36
36
  inboxAck?: boolean;
37
37
  activate?: boolean;
38
+ /**
39
+ * Mirror the company vault onto local disk. Off unless asked for: a bot
40
+ * reads and writes vault files on demand through the `hq` MCP server, so the
41
+ * sync loop is not installed at all rather than installed and left failing
42
+ * on a host that cannot finish a full-copy download.
43
+ */
44
+ syncFiles?: boolean;
38
45
  }
39
46
  export interface KitFallbackInfo {
40
47
  mode: "fallback";
@@ -22,10 +22,10 @@ import * as os from "node:os";
22
22
  import { CLI_VERSION } from "../cli-version.js";
23
23
  import { ensureCognitoToken } from "../utils/cognito-session.js";
24
24
  import { readExternalMachineCreds, } from "../lib/agent-kit/creds.js";
25
- import { defaultKitConfig, readKitConfig, writeKitConfig, } from "../lib/agent-kit/kit-config.js";
25
+ import { defaultKitConfig, enabledKitComponents, readKitConfig, writeKitConfig, } from "../lib/agent-kit/kit-config.js";
26
26
  import { createKitLogger } from "../lib/agent-kit/log.js";
27
27
  import { agentKitPaths } from "../lib/agent-kit/paths.js";
28
- import { isKitService, KIT_SERVICES, kitServiceSpecs, } from "../lib/agent-kit/services.js";
28
+ import { isKitService, KIT_SERVICES, enabledKitServices, kitServiceSpec, kitServiceSpecs, } from "../lib/agent-kit/services.js";
29
29
  import { writeKitSkills } from "../lib/agent-kit/skills.js";
30
30
  import { runHeartbeatLoop } from "../lib/agent-kit/run/heartbeat.js";
31
31
  import { pollInboxOnce, runInboxLoop } from "../lib/agent-kit/run/inbox.js";
@@ -76,13 +76,24 @@ export function installKit(paths, opts, host, deps = {}, fallbackDeps = {}) {
76
76
  ...existing,
77
77
  ...(opts.hqRoot ? { hqRoot: opts.hqRoot } : {}),
78
78
  ...(opts.inboxAck !== undefined ? { inboxAck: opts.inboxAck } : {}),
79
+ ...(opts.syncFiles !== undefined ? { syncFiles: opts.syncFiles } : {}),
79
80
  cliVersion: CLI_VERSION,
80
81
  };
81
82
  fs.mkdirSync(config.hqRoot, { recursive: true, mode: 0o700 });
82
83
  writeKitConfig(paths, config);
83
84
  const skills = writeKitSkills(paths);
84
85
  const activate = opts.activate ?? deps.activate ?? true;
85
- const services = installServices(kitServiceSpecs(paths), host, {
86
+ // A reinstall that turns file sync off must also remove the unit an earlier
87
+ // install left behind, or the loop keeps running and keeps failing.
88
+ if (!config.syncFiles) {
89
+ try {
90
+ uninstallServices([kitServiceSpec("sync", paths)], host, deps);
91
+ }
92
+ catch {
93
+ /* best effort: an absent unit is the desired end state either way */
94
+ }
95
+ }
96
+ const services = installServices(kitServiceSpecs(paths, { syncFiles: config.syncFiles }), host, {
86
97
  ...deps,
87
98
  activate,
88
99
  });
@@ -149,6 +160,7 @@ export async function runKitService(service, rt) {
149
160
  intervalMs: rt.config.heartbeatIntervalMs,
150
161
  getToken: rt.getToken,
151
162
  log: rt.log,
163
+ components: enabledKitComponents(rt.config),
152
164
  });
153
165
  return;
154
166
  case "inbox":
@@ -210,9 +222,10 @@ export async function runKitService(service, rt) {
210
222
  }
211
223
  /** `kit run all`: supervise the four services as child processes with restart backoff. */
212
224
  export async function runAllKitServices(rt, host) {
213
- rt.log("info", `supervisor starting ${KIT_SERVICES.join(",")} agent=${rt.creds.entityUid} cli=${CLI_VERSION}`);
225
+ const services = enabledKitServices({ syncFiles: rt.config.syncFiles });
226
+ rt.log("info", `supervisor starting ${services.join(",")} agent=${rt.creds.entityUid} cli=${CLI_VERSION}`);
214
227
  const handle = superviseKitServices({
215
- services: KIT_SERVICES,
228
+ services,
216
229
  log: rt.log,
217
230
  spawnService: (service) => {
218
231
  const fd = fs.openSync(serviceLogPath(rt.paths, service), "a", 0o600);
@@ -265,21 +278,30 @@ export function registerAgentKitCommand(agent) {
265
278
  .description("Install, inspect, or run the external-agent background services");
266
279
  kit
267
280
  .command("install")
268
- .description("Install sync, work-mesh, inbox and heartbeat services plus the skills directory")
281
+ .description("Install the work-mesh, inbox and heartbeat services plus the skills directory")
269
282
  .option("--hq-root <path>", "Local HQ tree to sync the company vault into (default: ~/.hq-agent/hq)")
283
+ .option("--sync-files", "Also mirror the company vault to disk. Off by default: files are read on demand through the hq MCP server")
284
+ .option("--no-sync-files", "Do not mirror the vault to disk (default; removes the loop if an earlier install added it)")
270
285
  .option("--inbox-ack", "Ack mirrored inbox items on the server (default: leave acking to the bot)")
271
286
  .option("--no-activate", "Write unit files but do not load them")
272
287
  .option("--json", "Print machine-readable JSON")
273
288
  .action((opts) => {
274
289
  try {
275
290
  const paths = agentKitPaths(os.homedir(), process.env);
276
- const result = installKit(paths, { hqRoot: opts.hqRoot, inboxAck: opts.inboxAck, activate: opts.activate }, resolveServiceHost(paths));
291
+ const result = installKit(paths, {
292
+ hqRoot: opts.hqRoot,
293
+ inboxAck: opts.inboxAck,
294
+ syncFiles: opts.syncFiles,
295
+ activate: opts.activate,
296
+ }, resolveServiceHost(paths));
277
297
  if (opts.json) {
278
298
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
279
299
  return;
280
300
  }
281
301
  console.log(chalk.green("HQ agent kit installed."));
282
- console.log(` hq root: ${result.config.hqRoot}`);
302
+ console.log(result.config.syncFiles
303
+ ? ` files: mirrored to ${result.config.hqRoot}`
304
+ : " files: read on demand through the hq MCP server (no local copy)");
283
305
  console.log(` logs: ${paths.logsDir}`);
284
306
  console.log(` skills: ${paths.skillsDir} (${result.skills.length} skills)`);
285
307
  if (result.fallback) {
@@ -310,7 +332,7 @@ export function registerAgentKitCommand(agent) {
310
332
  try {
311
333
  const paths = agentKitPaths(os.homedir(), process.env);
312
334
  const fallback = fallbackStatus(paths);
313
- const result = servicesStatus(kitServiceSpecs(paths), resolveServiceHost(paths));
335
+ const result = servicesStatus(kitServiceSpecs(paths, { syncFiles: true }), resolveServiceHost(paths));
314
336
  if (opts.json) {
315
337
  console.log(JSON.stringify({ ok: true, ...result, ...(fallback ? { fallback } : {}) }, null, 2));
316
338
  return;
@@ -335,7 +357,7 @@ export function registerAgentKitCommand(agent) {
335
357
  try {
336
358
  const paths = agentKitPaths(os.homedir(), process.env);
337
359
  const fallback = stopFallback(paths);
338
- const result = uninstallServices(kitServiceSpecs(paths), resolveServiceHost(paths));
360
+ const result = uninstallServices(kitServiceSpecs(paths, { syncFiles: true }), resolveServiceHost(paths));
339
361
  if (opts.json) {
340
362
  console.log(JSON.stringify({ ok: true, ...result, fallback }, null, 2));
341
363
  return;
@@ -15,6 +15,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
15
15
  import { vaultApiFetch } from "../utils/vault-api.js";
16
16
  import { McpServer } from "../lib/agent-kit/mcp/jsonrpc.js";
17
17
  import { buildAgentMcpTools } from "../lib/agent-kit/mcp/tools.js";
18
+ import { readKitConfig } from "../lib/agent-kit/kit-config.js";
18
19
  import { agentKitPaths } from "../lib/agent-kit/paths.js";
19
20
  import { requireExternalCreds } from "./agent-kit.js";
20
21
  export const AGENT_MCP_SERVER_NAME = "hq-agent";
@@ -56,6 +57,7 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
56
57
  return {
57
58
  creds,
58
59
  paths,
60
+ syncFiles: readKitConfig(agentKitPaths(os.homedir(), process.env), CLI_VERSION).syncFiles,
59
61
  runHq: defaultRunHq(process.execPath, process.argv[1], env),
60
62
  getToken: () => ensureCognitoToken({ tokenSource: "machine", interactive: false }),
61
63
  apiJson: async (token, path, init) => {
@@ -3,7 +3,8 @@
3
3
  * team member, one line per check, non-zero exit on any failure:
4
4
  *
5
5
  * whoami mint as the machine identity; token names this agent
6
- * team-sync `hq sync pull --all` succeeds and the company folder exists
6
+ * files a vault listing answers on demand (or, with --sync-files, a
7
+ * pull succeeds and the company folder exists)
7
8
  * work-mesh the personal (contract-2) realtime vend names this agent, the
8
9
  * agent-authorized inbox route answers 200, and this host's own
9
10
  * presence is live: a fresh last-heartbeat.json plus an ok
@@ -46,7 +47,10 @@ export interface ProbeDeps {
46
47
  hqRoot: string;
47
48
  getToken: () => Promise<string>;
48
49
  resolveCompanyUid: (token: string, slug: string) => Promise<string>;
49
- runPull: (args: string[]) => Promise<number>;
50
+ /** Runs the `hq` CLI with these args as a child; resolves with its exit code. */
51
+ runHq: (args: string[]) => Promise<number>;
52
+ /** Mirror the vault to disk. Off by default: files are read through the API. */
53
+ syncFiles?: boolean;
50
54
  vendRealtime: (token: string) => Promise<{
51
55
  actorUid: string;
52
56
  }>;
@@ -3,7 +3,8 @@
3
3
  * team member, one line per check, non-zero exit on any failure:
4
4
  *
5
5
  * whoami mint as the machine identity; token names this agent
6
- * team-sync `hq sync pull --all` succeeds and the company folder exists
6
+ * files a vault listing answers on demand (or, with --sync-files, a
7
+ * pull succeeds and the company folder exists)
7
8
  * work-mesh the personal (contract-2) realtime vend names this agent, the
8
9
  * agent-authorized inbox route answers 200, and this host's own
9
10
  * presence is live: a fresh last-heartbeat.json plus an ok
@@ -91,29 +92,55 @@ export async function runProbe(deps) {
91
92
  checks.push({ name: "company", ok: false, detail: `cannot resolve ${deps.creds.companySlug}: ${errText(err)}` });
92
93
  }
93
94
  }
94
- // 2. team-sync
95
+ // 2. files
96
+ //
97
+ // Two shapes, because the kit has two. With file sync off (the default) the
98
+ // bot reads and writes vault files on demand, so what has to be proven is
99
+ // that a vault listing answers — not that a full local copy exists. Pulling
100
+ // the whole vault here would also make the probe fail for a reason that does
101
+ // not affect the bot: a team with a large asset library is hundreds of
102
+ // megabytes, and a sandboxed bot host whose egress truncates large responses
103
+ // can never finish it.
95
104
  if (token) {
96
- try {
97
- const code = await deps.runPull(syncPullArgs(deps.hqRoot));
98
- const companyDir = path.join(deps.hqRoot, "companies", deps.creds.companySlug);
99
- const present = fs.existsSync(companyDir);
100
- const ok = code === 0 && present;
101
- checks.push({
102
- name: "team-sync",
103
- ok,
104
- detail: ok
105
- ? `synced into ${companyDir}`
106
- : code !== 0
107
- ? `hq sync pull exited ${code}`
108
- : `sync exited 0 but ${companyDir} is missing`,
109
- });
105
+ if (deps.syncFiles) {
106
+ try {
107
+ const code = await deps.runHq(syncPullArgs(deps.hqRoot));
108
+ const companyDir = path.join(deps.hqRoot, "companies", deps.creds.companySlug);
109
+ const present = fs.existsSync(companyDir);
110
+ const ok = code === 0 && present;
111
+ checks.push({
112
+ name: "files",
113
+ ok,
114
+ detail: ok
115
+ ? `synced into ${companyDir}`
116
+ : code !== 0
117
+ ? `hq sync pull exited ${code}`
118
+ : `sync exited 0 but ${companyDir} is missing`,
119
+ });
120
+ }
121
+ catch (err) {
122
+ checks.push({ name: "files", ok: false, detail: errText(err) });
123
+ }
110
124
  }
111
- catch (err) {
112
- checks.push({ name: "team-sync", ok: false, detail: errText(err) });
125
+ else {
126
+ const target = `companies/${deps.creds.companySlug}`;
127
+ try {
128
+ const code = await deps.runHq(["files", "browse", target]);
129
+ checks.push({
130
+ name: "files",
131
+ ok: code === 0,
132
+ detail: code === 0
133
+ ? `${target} is readable on demand (no local copy)`
134
+ : `hq files browse ${target} exited ${code}`,
135
+ });
136
+ }
137
+ catch (err) {
138
+ checks.push({ name: "files", ok: false, detail: errText(err) });
139
+ }
113
140
  }
114
141
  }
115
142
  else {
116
- checks.push({ name: "team-sync", ok: false, detail: "skipped: no token" });
143
+ checks.push({ name: "files", ok: false, detail: "skipped: no token" });
117
144
  }
118
145
  // 3. work-mesh: vend + agent-authorized reachability + self presence
119
146
  if (token && companyUid) {
@@ -258,9 +285,10 @@ export function defaultProbeDeps(paths, creds) {
258
285
  paths,
259
286
  creds,
260
287
  hqRoot: config.hqRoot,
288
+ syncFiles: config.syncFiles,
261
289
  getToken: () => ensureCognitoToken({ tokenSource: "machine", interactive: false }),
262
290
  resolveCompanyUid: (token, slug) => getCompanyUid(token, slug),
263
- runPull: defaultRunPull(process.execPath, process.argv[1], process.env),
291
+ runHq: defaultRunPull(process.execPath, process.argv[1], process.env),
264
292
  vendRealtime: async (token) => {
265
293
  const bundle = await createPersonalRealtimeFetcher({ token, baseUrl: base })();
266
294
  return { actorUid: bundle.actorUid };
@@ -171,6 +171,7 @@ export interface ProvisionAgentInput {
171
171
  slug: string;
172
172
  codexAuthMode: "subscription" | "apiKey";
173
173
  provider?: "codex" | "grok" | "claude" | "agents-v2";
174
+ codexModel?: string;
174
175
  codexApiKey?: string;
175
176
  idempotencyKey: string;
176
177
  title?: string;
@@ -1074,7 +1074,8 @@ export function registerAgentsCommand(program) {
1074
1074
  .description("Provision a new cloud agent (company-specific monthly price shown before creation)")
1075
1075
  .option("--company <slug>", "Company slug (resolves to companyUid)")
1076
1076
  .option("--slug <slug>", "Agent slug (defaults to a slug of <name>)")
1077
- .option("--provider <provider>", "Runtime: codex | grok | claude | agents-v2 (default codex). claude is subscription-only; agents-v2 boots its brain box (codex, or grok via a grok-* model) and receives the v2 runtime post-boot")
1077
+ .option("--provider <provider>", "Runtime: agents-v2 (recommended) | codex | grok | claude (legacy). A --model selects agents-v2 by default")
1078
+ .option("--model <model>", "Brain model, for example gpt-5.5 or grok-4.6")
1078
1079
  .option("--auth-mode <mode>", "Auth: subscription | apiKey (default subscription)", "subscription")
1079
1080
  .option("--api-key-env <VAR>", "Env var holding the API key for --auth-mode apiKey (never pass the key as a flag)")
1080
1081
  .option("--title <title>", "Org-chart job title")
@@ -1088,7 +1089,13 @@ export function registerAgentsCommand(program) {
1088
1089
  // unrecognised value, silently provisioning a billable box the operator
1089
1090
  // did not ask for. They now exit(1) instead — see parseEnumOption.
1090
1091
  const authMode = parseEnumOption(opts.authMode, VALID_AUTH_MODES, "--auth-mode") ?? "subscription";
1091
- const provider = parseEnumOption(opts.provider, VALID_PROVIDERS, "--provider");
1092
+ const explicitProvider = parseEnumOption(opts.provider, VALID_PROVIDERS, "--provider");
1093
+ if (opts.model !== undefined &&
1094
+ !/^[A-Za-z0-9._-]{1,64}$/.test(opts.model)) {
1095
+ console.error(chalk.red("--model must be a model identifier (letters, digits, dot, dash, underscore; max 64 chars)."));
1096
+ process.exit(1);
1097
+ }
1098
+ const provider = explicitProvider ?? (opts.model ? "agents-v2" : undefined);
1092
1099
  // claude is subscription-only on hq-pro (rejectIncompatibleProviderAuthMode
1093
1100
  // returns AGENT_PROVIDER_INCOMPATIBLE_WITH_AUTH_MODE). Catch it here so the
1094
1101
  // operator gets a direct message instead of a 400 from the control plane
@@ -1139,6 +1146,7 @@ export function registerAgentsCommand(program) {
1139
1146
  slug,
1140
1147
  codexAuthMode: authMode,
1141
1148
  ...(provider ? { provider } : {}),
1149
+ ...(opts.model ? { codexModel: opts.model } : {}),
1142
1150
  ...(codexApiKey ? { codexApiKey } : {}),
1143
1151
  idempotencyKey,
1144
1152
  ...(opts.title ? { title: opts.title } : {}),
@@ -4,7 +4,7 @@
4
4
  * sync and how often. Kept separate from machine-creds.json, which is the
5
5
  * contract-shaped credential file and nothing else.
6
6
  */
7
- import type { AgentKitPaths } from "./paths.js";
7
+ import { type AgentKitPaths, type KitComponent } from "./paths.js";
8
8
  export declare const DEFAULT_SYNC_INTERVAL_MS: number;
9
9
  export declare const DEFAULT_INBOX_POLL_MS = 15000;
10
10
  export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
@@ -18,9 +18,34 @@ export interface KitConfig {
18
18
  meshRefreshMs: number;
19
19
  /** Ack mirrored inbox items on the server after writing them locally. */
20
20
  inboxAck: boolean;
21
+ /**
22
+ * Mirror the company vault onto local disk.
23
+ *
24
+ * Off by default. An external bot reads and writes vault files on demand
25
+ * through the `hq` MCP server (hq_files_read / hq_files_list /
26
+ * hq_files_write) and the `hq files` CLI, so a local mirror buys nothing
27
+ * for the common case and costs a full copy of the company's assets — on a
28
+ * team with a large brand library that is hundreds of megabytes, and on a
29
+ * sandboxed bot host whose egress proxy truncates large responses it can
30
+ * never finish at all (every file retried three times, all failing with
31
+ * ERR_HQ_TRUNCATED_RESPONSE, leaving the probe stuck and component-sync in
32
+ * error while the bot was otherwise a healthy team member).
33
+ *
34
+ * Turn it on with `hq agent kit install --sync-files` when the bot genuinely
35
+ * needs files on disk for shell-based work (grep, build tools, a local
36
+ * index). A kit.json written before this field existed is read as `true`,
37
+ * because every install of that vintage ran the sync loop.
38
+ */
39
+ syncFiles: boolean;
21
40
  installedAt: string;
22
41
  cliVersion: string;
23
42
  }
43
+ /**
44
+ * Kit components whose health this install actually reports. `sync` drops out
45
+ * when the vault is not mirrored to disk — there is no loop to be healthy or
46
+ * unhealthy, so reporting it either way would be a lie.
47
+ */
48
+ export declare function enabledKitComponents(config: Pick<KitConfig, "syncFiles">): readonly KitComponent[];
24
49
  export declare function defaultHqRoot(paths: Pick<AgentKitPaths, "agentDir">): string;
25
50
  export declare function defaultKitConfig(paths: Pick<AgentKitPaths, "agentDir">, cliVersion: string, now?: () => Date): KitConfig;
26
51
  export declare function writeKitConfig(paths: Pick<AgentKitPaths, "kitConfigPath">, config: KitConfig): void;
@@ -6,10 +6,19 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
+ import { KIT_COMPONENTS } from "./paths.js";
9
10
  export const DEFAULT_SYNC_INTERVAL_MS = 5 * 60_000;
10
11
  export const DEFAULT_INBOX_POLL_MS = 15_000;
11
12
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
12
13
  export const DEFAULT_MESH_REFRESH_MS = 5 * 60_000;
14
+ /**
15
+ * Kit components whose health this install actually reports. `sync` drops out
16
+ * when the vault is not mirrored to disk — there is no loop to be healthy or
17
+ * unhealthy, so reporting it either way would be a lie.
18
+ */
19
+ export function enabledKitComponents(config) {
20
+ return KIT_COMPONENTS.filter((c) => c !== "sync" || config.syncFiles);
21
+ }
13
22
  export function defaultHqRoot(paths) {
14
23
  return path.join(paths.agentDir, "hq");
15
24
  }
@@ -21,6 +30,7 @@ export function defaultKitConfig(paths, cliVersion, now = () => new Date()) {
21
30
  heartbeatIntervalMs: DEFAULT_HEARTBEAT_INTERVAL_MS,
22
31
  meshRefreshMs: DEFAULT_MESH_REFRESH_MS,
23
32
  inboxAck: false,
33
+ syncFiles: false,
24
34
  installedAt: now().toISOString(),
25
35
  cliVersion,
26
36
  };
@@ -47,6 +57,8 @@ export function readKitConfig(paths, cliVersion) {
47
57
  heartbeatIntervalMs: num("heartbeatIntervalMs"),
48
58
  meshRefreshMs: num("meshRefreshMs"),
49
59
  inboxAck: raw.inboxAck === true,
60
+ // Absent means a pre-`syncFiles` install, which always synced.
61
+ syncFiles: typeof raw.syncFiles === "boolean" ? raw.syncFiles : true,
50
62
  installedAt: typeof raw.installedAt === "string" ? raw.installedAt : base.installedAt,
51
63
  cliVersion: typeof raw.cliVersion === "string" ? raw.cliVersion : base.cliVersion,
52
64
  };
@@ -30,6 +30,13 @@ export interface McpToolClients {
30
30
  /** Run `hq <args…>` as the machine identity; never throws. */
31
31
  runHq: (args: string[]) => Promise<HqRunResult>;
32
32
  getToken: () => Promise<string>;
33
+ /**
34
+ * Whether this kit mirrors the vault to disk. Off by default, and it decides
35
+ * what `hq_search` can honestly do: the local qmd index only exists when
36
+ * there is a local copy to index, so without one the search is a server-side
37
+ * match on file names and paths, not on contents.
38
+ */
39
+ syncFiles?: boolean;
33
40
  /** Authenticated JSON call against the control plane. */
34
41
  apiJson: (token: string, path: string, init?: {
35
42
  method?: string;
@@ -99,7 +99,9 @@ export function buildAgentMcpTools(clients) {
99
99
  },
100
100
  {
101
101
  name: "hq_search",
102
- description: "Search the synced company vault (local qmd index). Modes: keyword (default), semantic, hybrid.",
102
+ description: clients.syncFiles
103
+ ? "Search the synced company vault (local qmd index). Modes: keyword (default), semantic, hybrid."
104
+ : "Search the company vault by file name and path, server-side (no local copy is kept). Content and semantic search need a kit installed with --sync-files.",
103
105
  inputSchema: {
104
106
  type: "object",
105
107
  properties: {
@@ -117,6 +119,14 @@ export function buildAgentMcpTools(clients) {
117
119
  throw new McpToolInputError('"mode" must be keyword, semantic or hybrid');
118
120
  }
119
121
  const count = int(args, "count", { min: 1, max: 50, fallback: 10 });
122
+ if (!clients.syncFiles) {
123
+ // No local copy means no local index. Say so plainly instead of
124
+ // running a local search that can only ever return nothing.
125
+ if (mode && mode !== "keyword") {
126
+ throw new McpToolInputError(`"${mode}" search needs a local copy of the vault; this agent reads files on demand. Use keyword, or reinstall the kit with \`hq agent kit install --sync-files\`.`);
127
+ }
128
+ return fromRun(await clients.runHq(["files", "search", query, "--company", company]), "hq files search");
129
+ }
120
130
  const hqArgs = ["search", query, "--mode", mode ?? "keyword", "-n", String(count), "--json"];
121
131
  return fromRun(await clients.runHq(hqArgs), "hq search");
122
132
  },
@@ -17,7 +17,15 @@ import { type AgentKitPaths, type KitComponent } from "../paths.js";
17
17
  /** A component stamp older than this reads as error. */
18
18
  export declare const COMPONENT_STALE_AFTER_MS: Record<KitComponent, number>;
19
19
  export interface HeartbeatBody {
20
- components: Record<KitComponent, ComponentStatus>;
20
+ /**
21
+ * Only the components this kit actually runs. A component that was never
22
+ * installed is OMITTED rather than reported `error`: with file sync off (the
23
+ * default — the bot reads vault files on demand through the MCP server)
24
+ * there is no sync loop to be unhealthy, and stamping it `error` would make
25
+ * every healthy bot look broken. hq-pro records the components it is sent
26
+ * and ignores absent names.
27
+ */
28
+ components: Partial<Record<KitComponent, ComponentStatus>>;
21
29
  cliVersion: string;
22
30
  hostInfo: {
23
31
  os: string;
@@ -26,10 +34,10 @@ export interface HeartbeatBody {
26
34
  cliVersion: string;
27
35
  };
28
36
  }
29
- export declare function buildHeartbeatBody(paths: Pick<AgentKitPaths, "stateDir">, now?: () => Date, hostInfo?: HeartbeatBody["hostInfo"]): HeartbeatBody;
37
+ export declare function buildHeartbeatBody(paths: Pick<AgentKitPaths, "stateDir">, now?: () => Date, hostInfo?: HeartbeatBody["hostInfo"], components_?: readonly KitComponent[]): HeartbeatBody;
30
38
  export interface LastHeartbeat {
31
39
  at: string;
32
- components: Record<KitComponent, ComponentStatus>;
40
+ components: Partial<Record<KitComponent, ComponentStatus>>;
33
41
  status: number;
34
42
  }
35
43
  export declare function writeLastHeartbeat(paths: Pick<AgentKitPaths, "stateDir" | "lastHeartbeatPath">, record: LastHeartbeat): void;
@@ -41,6 +49,8 @@ export interface HeartbeatDeps {
41
49
  intervalMs: number;
42
50
  getToken: () => Promise<string>;
43
51
  log: KitLogger;
52
+ /** Components this kit runs; defaults to all of them. */
53
+ components?: readonly KitComponent[];
44
54
  post?: (token: string, body: HeartbeatBody) => Promise<number>;
45
55
  now?: () => Date;
46
56
  sleep?: (ms: number) => Promise<void>;
@@ -23,9 +23,9 @@ export const COMPONENT_STALE_AFTER_MS = {
23
23
  mesh: 3 * 60_000,
24
24
  inbox: 3 * 60_000,
25
25
  };
26
- export function buildHeartbeatBody(paths, now = () => new Date(), hostInfo) {
26
+ export function buildHeartbeatBody(paths, now = () => new Date(), hostInfo, components_ = KIT_COMPONENTS) {
27
27
  const components = {};
28
- for (const c of KIT_COMPONENTS) {
28
+ for (const c of components_) {
29
29
  components[c] = readComponentStatus(paths, c, COMPONENT_STALE_AFTER_MS[c], now).status;
30
30
  }
31
31
  return {
@@ -67,7 +67,7 @@ export function defaultHeartbeatPost(agentUid, apiBaseUrl) {
67
67
  export async function beatOnce(deps) {
68
68
  const now = deps.now ?? (() => new Date());
69
69
  const post = deps.post ?? defaultHeartbeatPost(deps.agentUid, deps.apiBaseUrl);
70
- const body = buildHeartbeatBody(deps.paths, now);
70
+ const body = buildHeartbeatBody(deps.paths, now, undefined, deps.components ?? KIT_COMPONENTS);
71
71
  try {
72
72
  const token = await deps.getToken();
73
73
  const status = await post(token, body);
@@ -75,7 +75,9 @@ export async function beatOnce(deps) {
75
75
  if (ok) {
76
76
  writeLastHeartbeat(deps.paths, { at: now().toISOString(), components: body.components, status });
77
77
  }
78
- deps.log(ok ? "info" : "warn", `heartbeat ${status} sync=${body.components.sync} mesh=${body.components.mesh} inbox=${body.components.inbox}`);
78
+ deps.log(ok ? "info" : "warn", `heartbeat ${status} ${Object.entries(body.components)
79
+ .map(([name, health]) => `${name}=${health}`)
80
+ .join(" ")}`);
79
81
  return ok;
80
82
  }
81
83
  catch (err) {
@@ -17,5 +17,16 @@ export declare function isKitService(value: string): value is KitService;
17
17
  */
18
18
  export declare function kitServiceEnv(paths: AgentKitPaths): Record<string, string>;
19
19
  export declare function kitServiceSpec(service: KitService, paths: AgentKitPaths): ServiceSpec;
20
- export declare function kitServiceSpecs(paths: AgentKitPaths): ServiceSpec[];
20
+ /**
21
+ * Services to install. `sync` is included only when the kit is configured to
22
+ * mirror the vault to disk; by default a bot reads files on demand through the
23
+ * `hq` MCP server, so the loop (and its full-copy download) is not installed at
24
+ * all rather than installed and left failing.
25
+ */
26
+ export declare function kitServiceSpecs(paths: AgentKitPaths, options?: {
27
+ syncFiles?: boolean;
28
+ }): ServiceSpec[];
29
+ export declare function enabledKitServices(options?: {
30
+ syncFiles?: boolean;
31
+ }): readonly KitService[];
21
32
  //# sourceMappingURL=services.d.ts.map
@@ -40,7 +40,17 @@ export function kitServiceSpec(service, paths) {
40
40
  restartSec: 10,
41
41
  };
42
42
  }
43
- export function kitServiceSpecs(paths) {
44
- return KIT_SERVICES.map((s) => kitServiceSpec(s, paths));
43
+ /**
44
+ * Services to install. `sync` is included only when the kit is configured to
45
+ * mirror the vault to disk; by default a bot reads files on demand through the
46
+ * `hq` MCP server, so the loop (and its full-copy download) is not installed at
47
+ * all rather than installed and left failing.
48
+ */
49
+ export function kitServiceSpecs(paths, options = {}) {
50
+ return enabledKitServices(options).map((s) => kitServiceSpec(s, paths));
51
+ }
52
+ export function enabledKitServices(options = {}) {
53
+ const syncFiles = options.syncFiles === true;
54
+ return KIT_SERVICES.filter((s) => s !== "sync" || syncFiles);
45
55
  }
46
56
  //# sourceMappingURL=services.js.map
@@ -50,21 +50,29 @@ mark each item done.
50
50
  },
51
51
  {
52
52
  name: "search",
53
- description: "Search the company vault (documents, knowledge, meeting notes) with the hq CLI's local index.",
53
+ description: "Find things in the company vault (documents, knowledge, meeting notes) with the hq CLI.",
54
54
  body: `# HQ search
55
55
 
56
- The company vault is synced to the kit's HQ root (see \`~/.hq-agent/kit.json\`,
57
- \`hqRoot\`). Search it with:
56
+ By default this agent keeps no local copy of the vault, so search runs
57
+ server-side and matches file NAMES and paths, not file contents:
58
58
 
59
59
  \`\`\`bash
60
- hq search "<query>" # ranked results across the synced vault
61
- hq search "<query>" --mode hybrid # keyword + semantic
62
- hq search "<query>" --json # machine-readable
63
- hq files search "<query>" --company <slug> # server-side vault search (no local index needed)
60
+ hq files search "<query>" --company <slug> # find files by name/path
61
+ hq files browse companies/<slug>/<folder> # list a folder
62
+ hq files cat companies/<slug>/<file> # read one file
64
63
  \`\`\`
65
64
 
66
- If results look stale, the sync loop may not have run yet:
67
- \`hq sync pull --all --on-conflict keep --hq-root <hqRoot>\` pulls now.
65
+ A good pattern for a question you cannot answer from names alone: search or
66
+ browse to narrow to a few likely files, then read them with \`hq files cat\`.
67
+
68
+ Content and semantic search need a local copy and its index, which only exists
69
+ if the kit was installed with \`--sync-files\`. In that case (check
70
+ \`~/.hq-agent/kit.json\` → \`syncFiles\`) you also have:
71
+
72
+ \`\`\`bash
73
+ hq search "<query>" # ranked full-text over the local copy
74
+ hq search "<query>" --mode hybrid # keyword + semantic
75
+ \`\`\`
68
76
 
69
77
  Cite the file path of anything you quote so a teammate can open it.
70
78
  `,
@@ -82,11 +90,19 @@ hq files share companies/<slug>/<path> # share a path with a teammate
82
90
  hq files versions companies/<slug>/<file> # version history
83
91
  \`\`\`
84
92
 
85
- Paths are vault-relative and start with \`companies/<slug>/\`. The synced copy
86
- lives under the kit's HQ root (\`~/.hq-agent/kit.json\` \`hqRoot\`); edit files
87
- there and run \`hq sync push --hq-root <hqRoot>\` to publish. Writes are
88
- audited under this agent's identity only write where you were asked to,
89
- and prefer creating a new file over overwriting one you did not author.
93
+ Paths are vault-relative and start with \`companies/<slug>/\`. Read them on
94
+ demand with the commands above this agent keeps no local copy of the vault
95
+ unless the kit was installed with \`--sync-files\` (check \`~/.hq-agent/kit.json\`
96
+ \`syncFiles\`), so never assume a file is already on disk.
97
+
98
+ To publish a change you need that local copy: with \`syncFiles\` on, edit under
99
+ the kit's HQ root (\`kit.json\` → \`hqRoot\`) and run
100
+ \`hq sync push --hq-root <hqRoot>\`. Without it, \`hq files get\` a file, and if
101
+ you need to write one back, say so and ask — do not improvise an upload.
102
+
103
+ Writes are audited under this agent's identity — only write where you were
104
+ asked to, and prefer creating a new file over overwriting one you did not
105
+ author.
90
106
  `,
91
107
  },
92
108
  {
@@ -281,7 +281,9 @@ export function renderPlanLockNotice(status) {
281
281
  if (lock.reasons.includes("agents")) {
282
282
  lines.push(` Agents allowed on Starter: ${agentTarget(lock)}.`);
283
283
  }
284
- lines.push(" This workspace is read-only until it is fixed. Two ways out:");
284
+ lines.push(" Nothing has been deleted. This workspace is read-only until it is fixed:");
285
+ lines.push(" you can still delete things, but you cannot create, invite or upload.");
286
+ lines.push(" Two ways to fix it:");
285
287
  const remedy = lock.reasons.length
286
288
  ? lock.reasons.map((reason) => reasonRemedy(reason, lock)).join(", and ")
287
289
  : "come back under the Starter plan limits";
@@ -190,7 +190,7 @@ function withinDayWindow(shownAt, nowMs) {
190
190
  return nowMs - shownAt < DAY_MS;
191
191
  }
192
192
  function buildOverBox(overEntries, upgradeUrl) {
193
- const title = "⚠ HQ plan limit exceeded";
193
+ const title = "⚠ HQ Starter plan limit exceeded";
194
194
  const upgrade = `Upgrade: ${upgradeUrl}`;
195
195
  const resourceLines = overEntries.map(([key, entry]) => ` ${key}: ${entry.used}/${entry.limit}${dimensionSuffix(key)}`);
196
196
  const contentLines = [title, "", ...resourceLines, "", upgrade];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.118.1",
3
+ "version": "5.119.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {