@indigoai-us/hq-cli 5.124.0 → 5.126.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 (80) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
  3. package/assets/bot-workers/setup/skills/your-business.md +270 -0
  4. package/assets/bot-workers/setup/worker.yaml +189 -58
  5. package/dist/command-catalog.generated.d.ts +414 -5
  6. package/dist/command-catalog.generated.js +531 -5
  7. package/dist/command-registration-plan.d.ts +6 -0
  8. package/dist/command-registration-plan.js +1 -0
  9. package/dist/commands/bot.d.ts +10 -0
  10. package/dist/commands/bot.js +6 -0
  11. package/dist/commands/lanes.d.ts +75 -0
  12. package/dist/commands/lanes.js +2032 -0
  13. package/dist/commands/login.js +49 -6
  14. package/dist/commands/mesh.js +133 -28
  15. package/dist/commands/onboard-create-identity.d.ts +26 -0
  16. package/dist/commands/onboard-create-identity.js +23 -0
  17. package/dist/commands/onboard.js +13 -4
  18. package/dist/lib/bot/progress.d.ts +6 -0
  19. package/dist/lib/bot/progress.js +16 -0
  20. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  21. package/dist/lib/bot/runtime/grok.js +86 -8
  22. package/dist/lib/bot/runtime/index.js +15 -8
  23. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  24. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  25. package/dist/lib/doctor/checks/work-context.js +4 -2
  26. package/dist/lib/lanes/accounts.d.ts +43 -0
  27. package/dist/lib/lanes/accounts.js +128 -0
  28. package/dist/lib/lanes/config.d.ts +56 -0
  29. package/dist/lib/lanes/config.js +351 -0
  30. package/dist/lib/lanes/errors.d.ts +39 -0
  31. package/dist/lib/lanes/errors.js +72 -0
  32. package/dist/lib/lanes/index.d.ts +19 -0
  33. package/dist/lib/lanes/index.js +12 -0
  34. package/dist/lib/lanes/liveness.d.ts +89 -0
  35. package/dist/lib/lanes/liveness.js +273 -0
  36. package/dist/lib/lanes/registry.d.ts +103 -0
  37. package/dist/lib/lanes/registry.js +247 -0
  38. package/dist/lib/lanes/store.d.ts +64 -0
  39. package/dist/lib/lanes/store.js +627 -0
  40. package/dist/lib/lanes/throttle.d.ts +89 -0
  41. package/dist/lib/lanes/throttle.js +297 -0
  42. package/dist/lib/lanes/types.d.ts +193 -0
  43. package/dist/lib/lanes/types.js +217 -0
  44. package/dist/lib/mesh/live/backfill-held.d.ts +2 -0
  45. package/dist/lib/mesh/live/backfill-held.js +8 -4
  46. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +3 -0
  47. package/dist/lib/mesh/live/daemon/board-refresh.js +6 -0
  48. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  49. package/dist/lib/mesh/live/daemon/config.js +75 -0
  50. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  51. package/dist/lib/mesh/live/daemon/doctor.js +21 -1
  52. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  53. package/dist/lib/mesh/live/daemon/index.js +3 -3
  54. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  55. package/dist/lib/mesh/live/daemon/install.js +444 -2
  56. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  57. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  58. package/dist/lib/mesh/live/daemon/run.d.ts +11 -0
  59. package/dist/lib/mesh/live/daemon/run.js +129 -10
  60. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  61. package/dist/lib/mesh/live/flush.d.ts +3 -0
  62. package/dist/lib/mesh/live/flush.js +11 -0
  63. package/dist/lib/scan-packages/index.js +17 -12
  64. package/dist/lib/work-context/company.d.ts +8 -0
  65. package/dist/lib/work-context/company.js +62 -18
  66. package/dist/lib/work-context/config.d.ts +55 -0
  67. package/dist/lib/work-context/config.js +333 -67
  68. package/dist/lib/work-context/contract.d.ts +2 -0
  69. package/dist/lib/work-context/default-company.d.ts +43 -0
  70. package/dist/lib/work-context/default-company.js +83 -0
  71. package/dist/lib/work-context/outbox.d.ts +5 -1
  72. package/dist/lib/work-context/outbox.js +5 -0
  73. package/dist/lib/work-context/reconcile.d.ts +4 -0
  74. package/dist/lib/work-context/reconcile.js +86 -9
  75. package/dist/lib/work-context/state.d.ts +2 -0
  76. package/dist/lib/work-context/state.js +14 -0
  77. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  78. package/dist/lib/work-context/windows-acl.js +425 -0
  79. package/dist/utils/pack-contributions.js +5 -4
  80. package/package.json +1 -1
@@ -4,6 +4,11 @@
4
4
  import chalk from 'chalk';
5
5
  import { browserLogin, loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
6
6
  import { cognitoConfigForLoginProvider } from '../utils/login-provider.js';
7
+ import * as readline from "node:readline/promises";
8
+ import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
9
+ import { setDefaultCompany } from "../lib/work-context/config.js";
10
+ import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
11
+ import { workContextRoot } from "../lib/work-context/paths.js";
7
12
  function peekIdToken(idToken) {
8
13
  try {
9
14
  const payload = idToken.split('.')[1];
@@ -24,21 +29,59 @@ export function registerLoginCommand(program) {
24
29
  .description('Authenticate with HQ via Cognito')
25
30
  .option('--provider <provider>', 'OAuth provider to use: google, microsoft, or picker')
26
31
  .action(async (options) => {
32
+ let accessToken;
27
33
  try {
28
34
  const existing = loadCachedTokens();
29
35
  if (existing && !isExpiring(existing, 120)) {
30
36
  const who = peekIdToken(existing.idToken).email ?? 'cached session';
31
37
  console.log(chalk.green(`Already logged in as ${who}. Run \`hq logout\` to switch accounts.`));
32
- return;
38
+ accessToken = existing.accessToken;
39
+ }
40
+ else {
41
+ console.log('Opening browser for authentication...');
42
+ const tokens = await browserLogin(cognitoConfigForLoginProvider(options.provider));
43
+ const who = peekIdToken(tokens.idToken).email ?? 'HQ';
44
+ console.log(chalk.green(`Logged in as ${who}`));
45
+ accessToken = tokens.accessToken;
33
46
  }
34
- console.log('Opening browser for authentication...');
35
- const tokens = await browserLogin(cognitoConfigForLoginProvider(options.provider));
36
- const who = peekIdToken(tokens.idToken).email ?? 'HQ';
37
- console.log(chalk.green(`Logged in as ${who}`));
38
47
  }
39
48
  catch (error) {
40
49
  console.error(chalk.red('Login failed:'), error instanceof Error ? error.message : 'Unknown error');
41
- process.exit(1);
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+ try {
54
+ const root = workContextRoot();
55
+ const activation = await ensureDefaultCompany({
56
+ root,
57
+ listMemberships: () => listActiveMembershipCompanies(accessToken),
58
+ log: (message) => console.error(chalk.dim(message)),
59
+ });
60
+ if (activation.status === "needs_choice" && process.stdin.isTTY && process.stdout.isTTY) {
61
+ const choices = activation.memberships
62
+ .map((membership, index) => `${index + 1}. ${membership.slug}`)
63
+ .join("\n");
64
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
65
+ try {
66
+ const answer = await rl.question(`Choose this device's default company:\n${choices}\nSelection: `);
67
+ const selected = activation.memberships[Number.parseInt(answer, 10) - 1];
68
+ if (selected) {
69
+ await setDefaultCompany(selected.slug, {
70
+ root,
71
+ validateMembership: (candidate) => candidate === selected.slug ? { uid: selected.uid } : false,
72
+ });
73
+ }
74
+ }
75
+ finally {
76
+ rl.close();
77
+ }
78
+ }
79
+ else if (activation.status === "needs_choice") {
80
+ console.error(chalk.yellow("Multiple active memberships. Run `hq mesh context default set <slug>` to choose this device's default company."));
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error(chalk.yellow('Default-company setup failed:'), error instanceof Error ? error.message : 'Unknown error');
42
85
  }
43
86
  });
44
87
  }
@@ -10,15 +10,17 @@ import { findHqRoot } from "../utils/manifest.js";
10
10
  import chalk from "chalk";
11
11
  import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
12
12
  import * as readline from "node:readline/promises";
13
- import { STORY_STATUSES, appendThreadEvent, callerLabelFromToken, ensureProjectThread, eventPayload, listActiveMembershipCompanies, listActiveThreads, patchStoryStatus, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
13
+ import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, listActiveMembershipCompanies, listActiveThreads, patchStoryStatus, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
14
14
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
15
- import { clearDefaultCompany, getDefaultCompany, readDeviceConfig, recordMigrationCapabilitySnapshot, setDefaultCompany, } from "../lib/work-context/config.js";
15
+ import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
16
+ import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
16
17
  import { DefaultCompanyUnavailableError, } from "../lib/work-context/errors.js";
17
18
  import { isValidSessionId } from "../lib/mesh/live/session-identity.js";
18
19
  import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/index.js";
19
20
  import { flushSessionEvents } from "../lib/mesh/live/flush.js";
20
21
  import { backfillHeldSessions } from "../lib/mesh/live/backfill-held.js";
21
22
  import { createSessionEventsPoster, resolveVaultApiBase, } from "../lib/mesh/live/session-events-client.js";
23
+ const MEMBERSHIP_VALIDATION_TIMEOUT_MS = 5_000;
22
24
  import { buildEmitEvidence } from "../lib/mesh/live/emit-evidence.js";
23
25
  import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
24
26
  import { emitEvents } from "../lib/mesh/live/emit.js";
@@ -27,7 +29,7 @@ import { generateUlid } from "../lib/mesh/live/ulid.js";
27
29
  import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
28
30
  import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
29
31
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
30
- import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
32
+ import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
31
33
  import { workContextRoot } from "../lib/work-context/paths.js";
32
34
  import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
33
35
  import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
@@ -91,7 +93,10 @@ async function runEvent(verb, opts) {
91
93
  summary: opts.summary,
92
94
  });
93
95
  const eventKind = verb === "start" ? "claim" : verb;
94
- const event = await appendThreadEvent(token, company.companyUid, ensured.threadId, eventKind, eventPayload(eventKind, opts, callerLabelFromToken(token)));
96
+ const claimant = eventKind === "claim"
97
+ ? await resolveMeshPrincipalUid(token, loadCachedTokens())
98
+ : "";
99
+ const event = await appendThreadEvent(token, company.companyUid, ensured.threadId, eventKind, eventPayload(eventKind, opts, claimant));
95
100
  if (opts.json) {
96
101
  console.log(JSON.stringify({
97
102
  ok: true,
@@ -153,25 +158,73 @@ async function runContextReconcile(opts) {
153
158
  let fetchCandidates;
154
159
  let validateMembership;
155
160
  let offline = Boolean(opts.offline);
156
- if (!offline) {
157
- try {
158
- const token = await requireToken();
159
- deliver = createWorkSessionDeliverer({ token });
160
- fetchCandidates = createCandidatesFetcher({ token });
161
- validateMembership = async (candidate) => {
162
- const membership = await resolveActiveMembershipCompany(token, candidate);
163
- if (!membership)
164
- return false;
165
- return { uid: membership.companyUid };
166
- };
161
+ let token;
162
+ try {
163
+ // Offline is outbox-only delivery. It still refreshes a device default's
164
+ // token-backed membership so a revoked default cannot be preflighted.
165
+ token = await requireToken();
166
+ const sessionToken = token;
167
+ validateMembership = async (candidate) => {
168
+ const membership = await new Promise((resolve, reject) => {
169
+ const timer = setTimeout(() => reject(new Error("membership validation timed out")), MEMBERSHIP_VALIDATION_TIMEOUT_MS);
170
+ void resolveActiveMembershipCompany(sessionToken, candidate).then((value) => {
171
+ clearTimeout(timer);
172
+ resolve(value);
173
+ }, (error) => {
174
+ clearTimeout(timer);
175
+ reject(error);
176
+ });
177
+ });
178
+ if (!membership || typeof membership.companyUid !== "string" || !membership.companyUid) {
179
+ return false;
180
+ }
181
+ return { uid: membership.companyUid };
182
+ };
183
+ if (!offline) {
184
+ deliver = createWorkSessionDeliverer({ token: sessionToken });
185
+ fetchCandidates = createCandidatesFetcher({ token: sessionToken });
167
186
  }
168
- catch {
187
+ }
188
+ catch {
189
+ if (!offline) {
169
190
  offline = true;
170
191
  if (!opts.machine) {
171
192
  console.error(chalk.yellow("No Cognito session; reconciling offline (outbox only)."));
172
193
  }
173
194
  }
174
195
  }
196
+ if (token) {
197
+ const activation = await ensureDefaultCompany({
198
+ root,
199
+ listMemberships: () => listActiveMembershipCompanies(token),
200
+ log: (message) => console.error(chalk.dim(message)),
201
+ });
202
+ if (activation.status === "needs_choice") {
203
+ const interactive = !opts.machine && Boolean(process.stdin.isTTY && process.stdout.isTTY) && !isMachineIdentity();
204
+ if (interactive) {
205
+ const choices = activation.memberships
206
+ .map((membership, index) => `${index + 1}. ${membership.slug}`)
207
+ .join("\n");
208
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
209
+ try {
210
+ const answer = await rl.question(`Choose this device's default company:\n${choices}\nSelection: `);
211
+ const selected = activation.memberships[Number.parseInt(answer, 10) - 1];
212
+ if (selected) {
213
+ await setDefaultCompany(selected.slug, {
214
+ root,
215
+ validateMembership: (candidate) => candidate === selected.slug ? { uid: selected.uid } : false,
216
+ });
217
+ }
218
+ }
219
+ finally {
220
+ rl.close();
221
+ }
222
+ }
223
+ else if (!opts.machine) {
224
+ console.error(chalk.yellow("Multiple active memberships. Run `hq mesh context default set <slug>` to choose this device's default company."));
225
+ }
226
+ }
227
+ }
175
228
  const outcome = await reconcileObservation(obs, {
176
229
  root,
177
230
  env: process.env,
@@ -295,6 +348,7 @@ async function runContextBackfillHeld(opts) {
295
348
  const parsedLimit = opts.limit ? Number.parseInt(opts.limit, 10) : 0;
296
349
  const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 0;
297
350
  const companyHint = opts.company?.trim() || undefined;
351
+ const useDeviceDefault = opts.useDeviceDefault === true || readDeviceConfig({ root }).repairHeldWithDefault === true;
298
352
  // Build the reconcile network seam exactly like `hq mesh context reconcile`.
299
353
  // Not needed in --dry-run (reconcile is never invoked then). Explicit types
300
354
  // because these are captured in the reconcile closure below (no control-flow
@@ -331,6 +385,7 @@ async function runContextBackfillHeld(opts) {
331
385
  // Mirror the reconcile closure's non-forcing hint in the dry-run predictor
332
386
  // so `--dry-run` and the real run report the same counts.
333
387
  remoteOwnerHint: companyHint || undefined,
388
+ useDeviceDefault,
334
389
  reconcile: (obs) => reconcileObservation(
335
390
  // Pass --company as a non-forcing hint (remoteOwnerSlug feeds
336
391
  // deterministic resolution BELOW the identity-file / device default,
@@ -342,6 +397,8 @@ async function runContextBackfillHeld(opts) {
342
397
  fetchCandidates,
343
398
  offline,
344
399
  validateMembership,
400
+ allowDeviceDefault: useDeviceDefault,
401
+ deviceDefaultConfidence: useDeviceDefault,
345
402
  }),
346
403
  });
347
404
  if (opts.json) {
@@ -364,16 +421,34 @@ async function runContextBackfillHeld(opts) {
364
421
  async function runContextDefaultGet(opts) {
365
422
  const root = workContextHomeRoot();
366
423
  const cfg = readDeviceConfig({ root });
367
- const current = getDefaultCompany({ root });
424
+ const current = cfg.defaultCompany?.enabled && cfg.defaultCompany.slug
425
+ ? cfg.defaultCompany
426
+ : null;
427
+ const memberships = cfg.defaultCompanyMemberships?.memberships ?? [];
428
+ const disabled = isDefaultCompanyDisabled({ root });
429
+ const source = disabled ? "disabled" : current?.source ?? (current ? "chosen" : "unset");
368
430
  if (opts.json) {
369
- console.log(JSON.stringify({ ok: true, config: cfg, defaultCompany: current }, null, 2));
431
+ console.log(JSON.stringify({
432
+ ok: true,
433
+ slug: current?.slug ?? null,
434
+ uid: current?.uid ?? null,
435
+ enabled: current?.enabled ?? false,
436
+ source,
437
+ needsChoice: !current && !disabled && memberships.length > 1,
438
+ memberships,
439
+ repairHeldWithDefault: cfg.repairHeldWithDefault === true,
440
+ }, null, 2));
441
+ return;
442
+ }
443
+ if (disabled) {
444
+ console.log("Default company disabled on this device.");
370
445
  return;
371
446
  }
372
447
  if (!current) {
373
448
  console.log("No device default company (default-company mode may be dark).");
374
449
  return;
375
450
  }
376
- console.log(`Default company: ${current.slug}${current.uid ? ` (${current.uid})` : ""} enabled=${current.enabled}`);
451
+ console.log(`Default company: ${current.slug}${current.uid ? ` (${current.uid})` : ""} enabled=${current.enabled} source=${source}`);
377
452
  }
378
453
  async function runContextDefaultSet(slug, opts) {
379
454
  const root = workContextHomeRoot();
@@ -434,11 +509,14 @@ async function runContextDefaultSet(slug, opts) {
434
509
  return { uid: membership.companyUid };
435
510
  },
436
511
  });
512
+ const configured = opts.repairHeldWithDefault
513
+ ? setRepairHeldWithDefault(true, { root })
514
+ : cfg;
437
515
  if (opts.json) {
438
- console.log(JSON.stringify({ ok: true, config: cfg, migrationWarning }, null, 2));
516
+ console.log(JSON.stringify({ ok: true, config: configured, migrationWarning }, null, 2));
439
517
  return;
440
518
  }
441
- const stored = cfg.defaultCompany;
519
+ const stored = configured.defaultCompany;
442
520
  console.log(`Default company set to ${stored?.slug ?? slug}${stored?.uid ? ` (${stored.uid})` : ""}`);
443
521
  if (migrationWarning)
444
522
  console.error(chalk.yellow(migrationWarning));
@@ -1035,7 +1113,8 @@ export function registerMeshCommand(program) {
1035
1113
  .option("--dry-run", "Report what would be reconciled; write nothing, no network")
1036
1114
  .option("--offline", "Reconcile outbox-only (no candidates/register network)")
1037
1115
  .option("--limit <n>", "Cap sessions processed (default 0 = no cap)")
1038
- .option("--company <slug>", "Optional non-forcing company hint (resolver default still wins)")
1116
+ .option("--company <slug>", "Optional non-forcing company hint")
1117
+ .option("--use-device-default", "Opt in to historical repair using the device default (lower-confidence attribution)")
1039
1118
  .option("--json", "Print machine-readable JSON")
1040
1119
  .action((opts) => wrap(() => runContextBackfillHeld(opts))());
1041
1120
  context
@@ -1090,6 +1169,7 @@ export function registerMeshCommand(program) {
1090
1169
  .description("Set the device default company slug after verifying membership")
1091
1170
  .argument("<slug>", "Company slug")
1092
1171
  .option("--allow-without-migration", "Deprecated compatibility no-op; selected-company membership is always verified")
1172
+ .option("--repair-held-with-default", "Persist opt-in for lower-confidence historical held-session repair")
1093
1173
  .option("--company <slug|uid>", "Deprecated no-op; the selected company is verified directly")
1094
1174
  .option("--json", "Print machine-readable JSON")
1095
1175
  .action((slug, opts) => wrap(() => runContextDefaultSet(slug, opts))());
@@ -1150,11 +1230,13 @@ export function registerMeshCommand(program) {
1150
1230
  daemon
1151
1231
  .command("run")
1152
1232
  .description("Start the single-instance daemon (pid lock, spool watch, presence)")
1153
- .action(() => wrap(() => runDaemonRun())());
1233
+ .option("--hq-root <path>", "HQ tree root (else HQ_ROOT / persisted trusted roots); the Windows task passes this explicitly")
1234
+ .action((opts) => wrap(() => runDaemonRun(opts))());
1154
1235
  daemon
1155
1236
  .command("install")
1156
- .description("Install LaunchAgent (macOS) or systemd user unit (Linux)")
1237
+ .description("Install LaunchAgent (macOS), systemd user unit (Linux), or Scheduled Task (Windows)")
1157
1238
  .option("--system-user <user>", "Write a root-owned unit template for agent boxes (e.g. ec2-user)")
1239
+ .option("--hq-root <path>", "HQ root to record for the service (else HQ_ROOT, else the nearest HQ root above cwd)")
1158
1240
  .option("--json", "Print machine-readable JSON")
1159
1241
  .action((opts) => wrap(() => runDaemonInstall(opts))());
1160
1242
  daemon
@@ -1173,19 +1255,24 @@ export function registerMeshCommand(program) {
1173
1255
  .option("--json", "Print machine-readable JSON")
1174
1256
  .action((opts) => wrap(() => runDaemonDoctorCmd(opts))());
1175
1257
  }
1176
- async function runDaemonRun() {
1177
- const handle = await runMeshDaemon();
1258
+ async function runDaemonRun(opts = {}) {
1259
+ const hqRoot = opts.hqRoot?.trim() || undefined;
1260
+ const handle = await runMeshDaemon(hqRoot ? { hqRoot } : {});
1178
1261
  // Stay alive until signal handlers call stop + exit.
1179
1262
  await new Promise(() => {
1180
1263
  void handle;
1181
1264
  });
1182
1265
  }
1183
1266
  async function runDaemonInstall(opts) {
1267
+ // Verified root only: a repo's own .claude/ must never become the daemon's
1268
+ // HQ root (that path is persisted as trusted and used for attribution).
1269
+ const hqRoot = resolveInstallHqRoot({ explicit: opts.hqRoot });
1184
1270
  const result = installDaemonService({
1185
1271
  platform: detectPlatform(),
1186
1272
  paths: buildInstallPaths({
1187
1273
  systemUser: opts.systemUser,
1188
- hqRoot: process.env.HQ_ROOT?.trim() || findHqRoot(),
1274
+ hqRoot: hqRoot.root,
1275
+ hqRootSource: hqRoot.source,
1189
1276
  }),
1190
1277
  systemUser: opts.systemUser,
1191
1278
  });
@@ -1196,10 +1283,15 @@ async function runDaemonInstall(opts) {
1196
1283
  platform: result.platform,
1197
1284
  dest: result.dest,
1198
1285
  message: result.message,
1286
+ hqRoot: hqRoot.root,
1287
+ hqRootSource: hqRoot.source,
1288
+ ...(result.notice ? { notice: result.notice } : {}),
1199
1289
  }, null, 2));
1200
1290
  return;
1201
1291
  }
1202
1292
  console.log(result.message);
1293
+ if (result.notice)
1294
+ console.log(result.notice);
1203
1295
  }
1204
1296
  async function runDaemonUninstall(opts) {
1205
1297
  const result = uninstallDaemonService({
@@ -1233,12 +1325,21 @@ async function runDaemonStatus(opts) {
1233
1325
  running: result.running ?? false,
1234
1326
  dest: result.dest,
1235
1327
  message: result.message,
1328
+ ...(result.lastResult !== undefined ? { lastResult: result.lastResult } : {}),
1329
+ ...(result.lastRunFailed !== undefined ? { lastRunFailed: result.lastRunFailed } : {}),
1330
+ ...(result.recordedHqRoot !== undefined ? { hqRoot: result.recordedHqRoot } : {}),
1331
+ ...(result.rootValid !== undefined ? { rootValid: result.rootValid } : {}),
1332
+ ...(result.notice ? { notice: result.notice } : {}),
1236
1333
  mqttState: state?.mqttState,
1237
1334
  presenceRefusal,
1238
1335
  }, null, 2));
1239
1336
  return;
1240
1337
  }
1241
1338
  console.log(result.message);
1339
+ if (result.recordedHqRoot)
1340
+ console.log(`hq root: ${result.recordedHqRoot}`);
1341
+ if (result.notice)
1342
+ console.log(result.notice);
1242
1343
  if (presenceRefusal) {
1243
1344
  console.log(`presence refusal: ${presenceRefusal.code} (HTTP ${presenceRefusal.status}); next retry at ${presenceRefusal.nextRetryAt}`);
1244
1345
  }
@@ -1247,7 +1348,11 @@ async function runDaemonStatus(opts) {
1247
1348
  }
1248
1349
  }
1249
1350
  async function runDaemonDoctorCmd(opts) {
1250
- const report = collectDaemonDoctor({ hqRoot: process.env.HQ_ROOT?.trim() || findHqRoot() });
1351
+ // Same predicate as install: a repo's .claude/ is not an HQ root. Doctor
1352
+ // only reads, so an unresolvable root is reported as absent, not fatal.
1353
+ const report = collectDaemonDoctor({
1354
+ hqRoot: process.env.HQ_ROOT?.trim() || findAncestorHqRoot(process.cwd()) || undefined,
1355
+ });
1251
1356
  if (opts.json) {
1252
1357
  console.log(JSON.stringify({ ok: !report.unhealthy, ...report }, null, 2));
1253
1358
  return;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Who a new company is created for, when `hq onboard create-company` is run
3
+ * without `--email` / `--person-name`.
4
+ *
5
+ * The signed-in account already carries both on its ID token. Asking the
6
+ * person to type their own name again — which is what the setup bot had to do,
7
+ * because `hq whoami` shows only the email — is a question HQ can answer
8
+ * itself. Explicit flags still win.
9
+ */
10
+ export interface CreateCompanyIdentity {
11
+ email: string;
12
+ personName: string;
13
+ }
14
+ export type CreateCompanyIdentityResult = {
15
+ ok: true;
16
+ identity: CreateCompanyIdentity;
17
+ } | {
18
+ ok: false;
19
+ message: string;
20
+ };
21
+ /** Pure: flags first, then the ID token's claims. */
22
+ export declare function resolveCreateCompanyIdentity(flags: {
23
+ email?: string;
24
+ personName?: string;
25
+ }, claims: Record<string, unknown>): CreateCompanyIdentityResult;
26
+ //# sourceMappingURL=onboard-create-identity.d.ts.map
@@ -0,0 +1,23 @@
1
+ function text(value) {
2
+ return typeof value === "string" ? value.trim() : "";
3
+ }
4
+ /** Pure: flags first, then the ID token's claims. */
5
+ export function resolveCreateCompanyIdentity(flags, claims) {
6
+ const email = text(flags.email) || text(claims.email);
7
+ const personName = text(flags.personName) ||
8
+ text(claims.name) ||
9
+ [text(claims.given_name), text(claims.family_name)].filter(Boolean).join(" ");
10
+ const missing = [];
11
+ if (!email)
12
+ missing.push("--email");
13
+ if (!personName)
14
+ missing.push("--person-name");
15
+ if (missing.length > 0) {
16
+ return {
17
+ ok: false,
18
+ message: `Your HQ sign-in does not say ${missing.length === 2 ? "your email or name" : missing[0] === "--email" ? "your email" : "your name"}. Pass ${missing.join(" and ")}.`,
19
+ };
20
+ }
21
+ return { ok: true, identity: { email, personName } };
22
+ }
23
+ //# sourceMappingURL=onboard-create-identity.js.map
@@ -20,11 +20,13 @@
20
20
  */
21
21
  import chalk from "chalk";
22
22
  import { runOnboardCli, readCheckpoint } from "../lib/onboarding/index.js";
23
- import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
23
+ import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoIdToken, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
24
24
  import { createDefaultVaultClient } from "./cloud-provision.js";
25
25
  import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
26
26
  import { planOnboardJoin } from "./onboard-join.js";
27
27
  import { buildCreateCompanyWarning } from "./onboard-warning.js";
28
+ import { resolveCreateCompanyIdentity } from "./onboard-create-identity.js";
29
+ import { peekIdToken } from "../utils/id-token.js";
28
30
  import { addBotToNewCompany, botToAddAfterCreateCompany } from "./onboard-bot-membership.js";
29
31
  // ---------------------------------------------------------------------------
30
32
  // Command registration
@@ -40,11 +42,18 @@ export function registerOnboardCommand(program) {
40
42
  "a same-named company makes a separate one you'd be alone in.")
41
43
  .requiredOption("--slug <slug>", "Company slug (used as bucket name suffix)")
42
44
  .requiredOption("--name <name>", "Company display name")
43
- .requiredOption("--email <email>", "Your email (must match Cognito sign-in)")
44
- .requiredOption("--person-name <name>", "Your display name")
45
+ .option("--email <email>", "Your email (default: your HQ sign-in's email; must match it)")
46
+ .option("--person-name <name>", "Your display name (default: the name on your HQ sign-in)")
45
47
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
46
- .action(async (options) => {
48
+ .action(async (flags) => {
47
49
  try {
50
+ const idToken = await ensureCognitoIdToken();
51
+ const resolved = resolveCreateCompanyIdentity(flags, peekIdToken(idToken));
52
+ if (!resolved.ok) {
53
+ console.error(chalk.red(`\n✗ ${resolved.message}`));
54
+ process.exit(1);
55
+ }
56
+ const options = { ...flags, ...resolved.identity };
48
57
  console.log(chalk.bold(`\nHQ Onboard — Create Company`));
49
58
  console.log(` Company: ${options.name} (${options.slug})`);
50
59
  console.log(` Person: ${options.personName} <${options.email}>`);
@@ -15,6 +15,10 @@
15
15
  * - the final answer is posted only when it adds something: text already
16
16
  * posted (or about to be) is never repeated, including Grok's final result
17
17
  * that is just every message concatenated
18
+ * - an in-between message that asks the person something (it has a "?") is
19
+ * held, not posted: the model often asks, keeps working, then asks the same
20
+ * thing again in its final answer, and the person read the question twice.
21
+ * It goes out only if the turn ends without a final answer of its own
18
22
  *
19
23
  * Posts are sent strictly in order. A failed post is logged by the caller's
20
24
  * `send` and does not stop the turn.
@@ -46,6 +50,8 @@ export declare class ProgressPoster {
46
50
  private readonly workingNoticeMs;
47
51
  private readonly workingNoticeText;
48
52
  private pending;
53
+ /** In-between questions, held until the turn ends (see the header). */
54
+ private heldQuestions;
49
55
  /** Every message the model has said this turn (posted or pending), in order. */
50
56
  private readonly said;
51
57
  private flushTimer;
@@ -15,6 +15,10 @@
15
15
  * - the final answer is posted only when it adds something: text already
16
16
  * posted (or about to be) is never repeated, including Grok's final result
17
17
  * that is just every message concatenated
18
+ * - an in-between message that asks the person something (it has a "?") is
19
+ * held, not posted: the model often asks, keeps working, then asks the same
20
+ * thing again in its final answer, and the person read the question twice.
21
+ * It goes out only if the turn ends without a final answer of its own
18
22
  *
19
23
  * Posts are sent strictly in order. A failed post is logged by the caller's
20
24
  * `send` and does not stop the turn.
@@ -37,6 +41,8 @@ export class ProgressPoster {
37
41
  workingNoticeMs;
38
42
  workingNoticeText;
39
43
  pending = [];
44
+ /** In-between questions, held until the turn ends (see the header). */
45
+ heldQuestions = [];
40
46
  /** Every message the model has said this turn (posted or pending), in order. */
41
47
  said = [];
42
48
  flushTimer = null;
@@ -75,6 +81,10 @@ export class ProgressPoster {
75
81
  return;
76
82
  this.cancelNotice();
77
83
  this.said.push(t);
84
+ if (t.includes("?")) {
85
+ this.heldQuestions.push(t);
86
+ return;
87
+ }
78
88
  this.pending.push(t);
79
89
  this.scheduleFlush();
80
90
  }
@@ -97,8 +107,12 @@ export class ProgressPoster {
97
107
  this.cancelNotice();
98
108
  this.cancelFlush();
99
109
  const final = finalText.trim();
110
+ const held = this.heldQuestions;
111
+ this.heldQuestions = [];
100
112
  if (final && !this.repeatsSaid(final))
101
113
  this.pending.push(final);
114
+ else if (held.length > 0)
115
+ this.pending.push(...held);
102
116
  if (this.pending.length === 0 && this.delivered === 0 && !final)
103
117
  this.pending.push("(no reply)");
104
118
  this.flushNow();
@@ -114,6 +128,8 @@ export class ProgressPoster {
114
128
  this.finished = true;
115
129
  this.cancelNotice();
116
130
  this.cancelFlush();
131
+ this.pending.push(...this.heldQuestions);
132
+ this.heldQuestions = [];
117
133
  this.flushNow();
118
134
  return this.chain;
119
135
  }
@@ -1,16 +1,39 @@
1
1
  /**
2
2
  * Grok CLI headless adapter.
3
3
  *
4
- * grok -p <prompt> --output-format streaming-messages-json --always-approve
4
+ * grok -p <prompt> --output-format <streaming format> --always-approve
5
5
  * --no-subagents --cwd <cwd> [-m m] [-r <sessionId>]
6
6
  *
7
- * stdout is NDJSON in the Anthropic Messages shape (see messages-stream.ts).
8
- * The plain `json` format glued every interim sentence into one reply; the
9
- * streaming format lets each message be posted as it is written.
10
- * Verified against grok (grok-4.6) on 2026-09-12 (fixtures/grok-stream.ndjson);
11
- * the older `{ text, sessionId }` envelope is still accepted.
7
+ * stdout is NDJSON. Older grok (before 0.2.56) wrote Anthropic Messages
8
+ * shapes under `--output-format streaming-messages-json` (see
9
+ * messages-stream.ts). grok 0.2.56 dropped that value and emits token
10
+ * events under `streaming-json`:
11
+ *
12
+ * {"type":"thought","data":…}
13
+ * {"type":"text","data":"Hi"}
14
+ * {"type":"end","stopReason":"EndTurn","sessionId":…}
15
+ *
16
+ * The format is probed from `grok --help` once per process (and cached).
17
+ * If an older grok still lists `streaming-messages-json`, that is used.
18
+ * If the probe misses and grok rejects the old value, the turn retries
19
+ * with `streaming-json`. The older `{ text, sessionId }` envelope is
20
+ * still accepted.
12
21
  */
13
22
  import { type BotRuntime, type RuntimeTurnInput } from "./types.js";
23
+ export declare const GROK_OUTPUT_FORMAT_LEGACY = "streaming-messages-json";
24
+ export declare const GROK_OUTPUT_FORMAT_STREAMING_JSON = "streaming-json";
25
+ export declare function resetGrokOutputFormatCache(): void;
26
+ /** Pick the streaming `--output-format` grok's `--help` says it accepts. */
27
+ export declare function grokOutputFormatFromHelp(helpText: string): string;
28
+ /** Probe `grok --help` once and cache the streaming format. */
29
+ export declare function probeGrokOutputFormat(readHelp?: () => string): string;
30
+ /**
31
+ * When grok 0.2.56 rejects the pre-0.2.56 flag, rewrite argv to
32
+ * `streaming-json` and remember that for later turns.
33
+ */
34
+ export declare function grokRetryArgsAfterInvalidOutputFormat(args: string[], stderr: string): string[] | undefined;
14
35
  export declare function grokPrompt(input: RuntimeTurnInput): string;
15
- export declare function createGrokRuntime(): BotRuntime;
36
+ export declare function createGrokRuntime(opts?: {
37
+ grokHelp?: string;
38
+ }): BotRuntime;
16
39
  //# sourceMappingURL=grok.d.ts.map