@indigoai-us/hq-cli 5.125.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 (57) hide show
  1. package/CHANGELOG.md +118 -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 +407 -4
  6. package/dist/command-catalog.generated.js +522 -4
  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/mesh.js +37 -9
  14. package/dist/commands/onboard-create-identity.d.ts +26 -0
  15. package/dist/commands/onboard-create-identity.js +23 -0
  16. package/dist/commands/onboard.js +13 -4
  17. package/dist/lib/bot/progress.d.ts +6 -0
  18. package/dist/lib/bot/progress.js +16 -0
  19. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  20. package/dist/lib/bot/runtime/grok.js +86 -8
  21. package/dist/lib/bot/runtime/index.js +15 -8
  22. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  23. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  24. package/dist/lib/lanes/accounts.d.ts +43 -0
  25. package/dist/lib/lanes/accounts.js +128 -0
  26. package/dist/lib/lanes/config.d.ts +56 -0
  27. package/dist/lib/lanes/config.js +351 -0
  28. package/dist/lib/lanes/errors.d.ts +39 -0
  29. package/dist/lib/lanes/errors.js +72 -0
  30. package/dist/lib/lanes/index.d.ts +19 -0
  31. package/dist/lib/lanes/index.js +12 -0
  32. package/dist/lib/lanes/liveness.d.ts +89 -0
  33. package/dist/lib/lanes/liveness.js +273 -0
  34. package/dist/lib/lanes/registry.d.ts +103 -0
  35. package/dist/lib/lanes/registry.js +247 -0
  36. package/dist/lib/lanes/store.d.ts +64 -0
  37. package/dist/lib/lanes/store.js +627 -0
  38. package/dist/lib/lanes/throttle.d.ts +89 -0
  39. package/dist/lib/lanes/throttle.js +297 -0
  40. package/dist/lib/lanes/types.d.ts +193 -0
  41. package/dist/lib/lanes/types.js +217 -0
  42. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  43. package/dist/lib/mesh/live/daemon/config.js +75 -0
  44. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  45. package/dist/lib/mesh/live/daemon/index.js +3 -3
  46. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  47. package/dist/lib/mesh/live/daemon/install.js +444 -2
  48. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  49. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  50. package/dist/lib/scan-packages/index.js +17 -12
  51. package/dist/lib/work-context/config.d.ts +20 -0
  52. package/dist/lib/work-context/config.js +26 -5
  53. package/dist/lib/work-context/reconcile.js +28 -7
  54. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  55. package/dist/lib/work-context/windows-acl.js +425 -0
  56. package/dist/utils/pack-contributions.js +5 -4
  57. package/package.json +1 -1
@@ -10,7 +10,7 @@ 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
15
  import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
16
16
  import { ensureDefaultCompany } from "../lib/work-context/default-company.js";
@@ -29,7 +29,7 @@ import { generateUlid } from "../lib/mesh/live/ulid.js";
29
29
  import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
30
30
  import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
31
31
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
32
- 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";
33
33
  import { workContextRoot } from "../lib/work-context/paths.js";
34
34
  import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
35
35
  import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
@@ -93,7 +93,10 @@ async function runEvent(verb, opts) {
93
93
  summary: opts.summary,
94
94
  });
95
95
  const eventKind = verb === "start" ? "claim" : verb;
96
- 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));
97
100
  if (opts.json) {
98
101
  console.log(JSON.stringify({
99
102
  ok: true,
@@ -1227,11 +1230,13 @@ export function registerMeshCommand(program) {
1227
1230
  daemon
1228
1231
  .command("run")
1229
1232
  .description("Start the single-instance daemon (pid lock, spool watch, presence)")
1230
- .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))());
1231
1235
  daemon
1232
1236
  .command("install")
1233
- .description("Install LaunchAgent (macOS) or systemd user unit (Linux)")
1237
+ .description("Install LaunchAgent (macOS), systemd user unit (Linux), or Scheduled Task (Windows)")
1234
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)")
1235
1240
  .option("--json", "Print machine-readable JSON")
1236
1241
  .action((opts) => wrap(() => runDaemonInstall(opts))());
1237
1242
  daemon
@@ -1250,19 +1255,24 @@ export function registerMeshCommand(program) {
1250
1255
  .option("--json", "Print machine-readable JSON")
1251
1256
  .action((opts) => wrap(() => runDaemonDoctorCmd(opts))());
1252
1257
  }
1253
- async function runDaemonRun() {
1254
- const handle = await runMeshDaemon();
1258
+ async function runDaemonRun(opts = {}) {
1259
+ const hqRoot = opts.hqRoot?.trim() || undefined;
1260
+ const handle = await runMeshDaemon(hqRoot ? { hqRoot } : {});
1255
1261
  // Stay alive until signal handlers call stop + exit.
1256
1262
  await new Promise(() => {
1257
1263
  void handle;
1258
1264
  });
1259
1265
  }
1260
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 });
1261
1270
  const result = installDaemonService({
1262
1271
  platform: detectPlatform(),
1263
1272
  paths: buildInstallPaths({
1264
1273
  systemUser: opts.systemUser,
1265
- hqRoot: process.env.HQ_ROOT?.trim() || findHqRoot(),
1274
+ hqRoot: hqRoot.root,
1275
+ hqRootSource: hqRoot.source,
1266
1276
  }),
1267
1277
  systemUser: opts.systemUser,
1268
1278
  });
@@ -1273,10 +1283,15 @@ async function runDaemonInstall(opts) {
1273
1283
  platform: result.platform,
1274
1284
  dest: result.dest,
1275
1285
  message: result.message,
1286
+ hqRoot: hqRoot.root,
1287
+ hqRootSource: hqRoot.source,
1288
+ ...(result.notice ? { notice: result.notice } : {}),
1276
1289
  }, null, 2));
1277
1290
  return;
1278
1291
  }
1279
1292
  console.log(result.message);
1293
+ if (result.notice)
1294
+ console.log(result.notice);
1280
1295
  }
1281
1296
  async function runDaemonUninstall(opts) {
1282
1297
  const result = uninstallDaemonService({
@@ -1310,12 +1325,21 @@ async function runDaemonStatus(opts) {
1310
1325
  running: result.running ?? false,
1311
1326
  dest: result.dest,
1312
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 } : {}),
1313
1333
  mqttState: state?.mqttState,
1314
1334
  presenceRefusal,
1315
1335
  }, null, 2));
1316
1336
  return;
1317
1337
  }
1318
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);
1319
1343
  if (presenceRefusal) {
1320
1344
  console.log(`presence refusal: ${presenceRefusal.code} (HTTP ${presenceRefusal.status}); next retry at ${presenceRefusal.nextRetryAt}`);
1321
1345
  }
@@ -1324,7 +1348,11 @@ async function runDaemonStatus(opts) {
1324
1348
  }
1325
1349
  }
1326
1350
  async function runDaemonDoctorCmd(opts) {
1327
- 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
+ });
1328
1356
  if (opts.json) {
1329
1357
  console.log(JSON.stringify({ ok: !report.unhealthy, ...report }, null, 2));
1330
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
@@ -1,23 +1,101 @@
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
  */
22
+ import { execFileSync } from "node:child_process";
13
23
  import { messagesStreamEvents, messagesStreamFailure, messagesStreamResult } from "./messages-stream.js";
14
24
  import { parseJsonLine, RuntimeError } from "./types.js";
25
+ export const GROK_OUTPUT_FORMAT_LEGACY = "streaming-messages-json";
26
+ export const GROK_OUTPUT_FORMAT_STREAMING_JSON = "streaming-json";
27
+ let cachedOutputFormat;
28
+ export function resetGrokOutputFormatCache() {
29
+ cachedOutputFormat = undefined;
30
+ }
31
+ /** Pick the streaming `--output-format` grok's `--help` says it accepts. */
32
+ export function grokOutputFormatFromHelp(helpText) {
33
+ const section = /--output-format\s*<OUTPUT_FORMAT>[\s\S]*?\[possible values:\s*([^\]]+)\]/i.exec(helpText);
34
+ const values = (section?.[1] ?? "")
35
+ .split(",")
36
+ .map((s) => s.trim())
37
+ .filter(Boolean);
38
+ if (values.includes(GROK_OUTPUT_FORMAT_LEGACY))
39
+ return GROK_OUTPUT_FORMAT_LEGACY;
40
+ if (values.includes(GROK_OUTPUT_FORMAT_STREAMING_JSON))
41
+ return GROK_OUTPUT_FORMAT_STREAMING_JSON;
42
+ if (helpText.includes(GROK_OUTPUT_FORMAT_LEGACY))
43
+ return GROK_OUTPUT_FORMAT_LEGACY;
44
+ if (/\bstreaming-json\b/.test(helpText))
45
+ return GROK_OUTPUT_FORMAT_STREAMING_JSON;
46
+ return GROK_OUTPUT_FORMAT_LEGACY;
47
+ }
48
+ function defaultGrokHelp() {
49
+ try {
50
+ return execFileSync("grok", ["--help"], {
51
+ encoding: "utf8",
52
+ timeout: 5_000,
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ });
55
+ }
56
+ catch (err) {
57
+ const e = err;
58
+ return `${e.stdout ?? ""}\n${e.stderr ?? ""}`;
59
+ }
60
+ }
61
+ /** Probe `grok --help` once and cache the streaming format. */
62
+ export function probeGrokOutputFormat(readHelp = defaultGrokHelp) {
63
+ if (cachedOutputFormat)
64
+ return cachedOutputFormat;
65
+ let help;
66
+ try {
67
+ help = readHelp();
68
+ }
69
+ catch {
70
+ help = "";
71
+ }
72
+ cachedOutputFormat = grokOutputFormatFromHelp(help);
73
+ return cachedOutputFormat;
74
+ }
75
+ /**
76
+ * When grok 0.2.56 rejects the pre-0.2.56 flag, rewrite argv to
77
+ * `streaming-json` and remember that for later turns.
78
+ */
79
+ export function grokRetryArgsAfterInvalidOutputFormat(args, stderr) {
80
+ if (!/invalid value ['"]streaming-messages-json['"]/.test(stderr))
81
+ return undefined;
82
+ const i = args.indexOf("--output-format");
83
+ if (i < 0 || args[i + 1] === GROK_OUTPUT_FORMAT_STREAMING_JSON)
84
+ return undefined;
85
+ const next = args.slice();
86
+ next[i + 1] = GROK_OUTPUT_FORMAT_STREAMING_JSON;
87
+ cachedOutputFormat = GROK_OUTPUT_FORMAT_STREAMING_JSON;
88
+ return next;
89
+ }
15
90
  export function grokPrompt(input) {
16
91
  if (input.sessionId)
17
92
  return input.prompt;
18
93
  return `${input.systemPrompt}\n\n---\n\nMessage from your owner:\n\n${input.prompt}`;
19
94
  }
20
- export function createGrokRuntime() {
95
+ export function createGrokRuntime(opts = {}) {
96
+ const format = opts.grokHelp !== undefined
97
+ ? grokOutputFormatFromHelp(opts.grokHelp)
98
+ : (cachedOutputFormat ?? GROK_OUTPUT_FORMAT_LEGACY);
21
99
  return {
22
100
  id: "grok",
23
101
  binary: "grok",
@@ -26,7 +104,7 @@ export function createGrokRuntime() {
26
104
  "-p",
27
105
  grokPrompt(input),
28
106
  "--output-format",
29
- "streaming-messages-json",
107
+ format,
30
108
  "--always-approve",
31
109
  "--no-subagents",
32
110
  "--cwd",
@@ -5,7 +5,7 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { createClaudeRuntime } from "./claude.js";
7
7
  import { createCodexRuntime } from "./codex.js";
8
- import { createGrokRuntime } from "./grok.js";
8
+ import { createGrokRuntime, grokRetryArgsAfterInvalidOutputFormat, probeGrokOutputFormat } from "./grok.js";
9
9
  import { parseJsonLine, RuntimeError, turnTimeoutMs, stderrExcerpt, } from "./types.js";
10
10
  export * from "./types.js";
11
11
  export { createClaudeRuntime, claudePermissionMode } from "./claude.js";
@@ -16,6 +16,7 @@ export function runtimeFor(id, env = process.env) {
16
16
  return createClaudeRuntime(env);
17
17
  if (id === "codex")
18
18
  return createCodexRuntime();
19
+ probeGrokOutputFormat();
19
20
  return createGrokRuntime();
20
21
  }
21
22
  /**
@@ -220,15 +221,21 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
220
221
  }
221
222
  : undefined;
222
223
  const timeoutMs = deps.timeoutMs ?? turnTimeoutMs(env);
224
+ const spawnOptions = {
225
+ cwd: input.cwd,
226
+ env,
227
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
228
+ ...(onStdoutLine ? { onStdoutLine } : {}),
229
+ ...(input.signal ? { signal: input.signal } : {}),
230
+ };
223
231
  let result;
224
232
  try {
225
- result = await spawnImpl(binary, args, {
226
- cwd: input.cwd,
227
- env,
228
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
229
- ...(onStdoutLine ? { onStdoutLine } : {}),
230
- ...(input.signal ? { signal: input.signal } : {}),
231
- });
233
+ result = await spawnImpl(binary, args, spawnOptions);
234
+ if (runtime.id === "grok" && result.code !== 0 && !input.signal?.aborted) {
235
+ const retryArgs = grokRetryArgsAfterInvalidOutputFormat(args, result.stderr);
236
+ if (retryArgs)
237
+ result = await spawnImpl(binary, retryArgs, spawnOptions);
238
+ }
232
239
  }
233
240
  catch (err) {
234
241
  const code = err.code;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The NDJSON stream Claude Code (`--output-format stream-json --verbose`) and
3
- * Grok (`--output-format streaming-messages-json`) both write: Anthropic
3
+ * older Grok (`--output-format streaming-messages-json`) both write: Anthropic
4
4
  * Messages wire shapes, one event per line.
5
5
  *
6
6
  * {"type":"system","subtype":"init","session_id":…}
@@ -9,9 +9,18 @@
9
9
  * {"type":"user","message":{"content":[{"type":"tool_result",…}]}}
10
10
  * {"type":"result","subtype":"success","is_error":false,"result":"<last message>","session_id":…}
11
11
  *
12
- * Recorded from Claude Code 2.1.258 and grok (model grok-4.6) on 2026-09-12;
12
+ * grok 0.2.56 dropped `streaming-messages-json` for `streaming-json`, which
13
+ * is token deltas rather than whole messages:
14
+ *
15
+ * {"type":"thought","data":…}
16
+ * {"type":"text","data":"Hi"}
17
+ * {"type":"end","stopReason":"EndTurn","sessionId":…}
18
+ *
19
+ * Text `data` chunks are concatenated for the final reply and never emitted
20
+ * as progress (they are token deltas). Recorded from Claude Code 2.1.258 and
21
+ * grok (model grok-4.6) on 2026-09-12, plus grok 0.2.56 on 2026-09-21;
13
22
  * fixtures in runtime/fixtures/. Claude sends one content block per assistant
14
- * event; Grok sends a whole message (thinking + text + tool_use) per event.
23
+ * event; older Grok sends a whole message (thinking + text + tool_use) per event.
15
24
  */
16
25
  import { type RuntimeStreamEvent, type RuntimeTurnResult } from "./types.js";
17
26
  /** Progress events in one stream line: top-level assistant text and tool starts only. */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The NDJSON stream Claude Code (`--output-format stream-json --verbose`) and
3
- * Grok (`--output-format streaming-messages-json`) both write: Anthropic
3
+ * older Grok (`--output-format streaming-messages-json`) both write: Anthropic
4
4
  * Messages wire shapes, one event per line.
5
5
  *
6
6
  * {"type":"system","subtype":"init","session_id":…}
@@ -9,9 +9,18 @@
9
9
  * {"type":"user","message":{"content":[{"type":"tool_result",…}]}}
10
10
  * {"type":"result","subtype":"success","is_error":false,"result":"<last message>","session_id":…}
11
11
  *
12
- * Recorded from Claude Code 2.1.258 and grok (model grok-4.6) on 2026-09-12;
12
+ * grok 0.2.56 dropped `streaming-messages-json` for `streaming-json`, which
13
+ * is token deltas rather than whole messages:
14
+ *
15
+ * {"type":"thought","data":…}
16
+ * {"type":"text","data":"Hi"}
17
+ * {"type":"end","stopReason":"EndTurn","sessionId":…}
18
+ *
19
+ * Text `data` chunks are concatenated for the final reply and never emitted
20
+ * as progress (they are token deltas). Recorded from Claude Code 2.1.258 and
21
+ * grok (model grok-4.6) on 2026-09-12, plus grok 0.2.56 on 2026-09-21;
13
22
  * fixtures in runtime/fixtures/. Claude sends one content block per assistant
14
- * event; Grok sends a whole message (thinking + text + tool_use) per event.
23
+ * event; older Grok sends a whole message (thinking + text + tool_use) per event.
15
24
  */
16
25
  import { parseJsonLine, RuntimeError } from "./types.js";
17
26
  /** Progress events in one stream line: top-level assistant text and tool starts only. */
@@ -48,6 +57,9 @@ export function messagesStreamEvents(line) {
48
57
  * RuntimeError for an error result, or when there is nothing usable.
49
58
  */
50
59
  export function messagesStreamResult(stdout, runtimeName) {
60
+ const token = grokTokenStreamResult(stdout, runtimeName);
61
+ if (token)
62
+ return token;
51
63
  let result = null;
52
64
  let sessionId;
53
65
  let lastText;
@@ -78,6 +90,38 @@ export function messagesStreamResult(stdout, runtimeName) {
78
90
  sessionId = result.session_id;
79
91
  return { text, sessionId };
80
92
  }
93
+ /**
94
+ * grok 0.2.56 `--output-format streaming-json`: concatenate text deltas and
95
+ * take sessionId from the terminal `end` event. Throws on `error`. Null when
96
+ * the stream is not this shape (so the Anthropic Messages parser can run).
97
+ */
98
+ function grokTokenStreamResult(stdout, runtimeName) {
99
+ let text = "";
100
+ let sessionId;
101
+ let saw = false;
102
+ for (const line of stdout.split("\n")) {
103
+ const ev = parseJsonLine(line);
104
+ if (!ev)
105
+ continue;
106
+ if (ev.type === "text" && typeof ev.data === "string") {
107
+ saw = true;
108
+ text += ev.data;
109
+ }
110
+ else if (ev.type === "thought") {
111
+ saw = true;
112
+ }
113
+ else if (ev.type === "end") {
114
+ saw = true;
115
+ if (typeof ev.sessionId === "string")
116
+ sessionId = ev.sessionId;
117
+ }
118
+ else if (ev.type === "error" && typeof ev.message === "string") {
119
+ const detail = ev.message;
120
+ throw new RuntimeError(`${runtimeName} reported an error: ${detail.slice(0, 200)}`, detail.slice(0, 400), null);
121
+ }
122
+ }
123
+ return saw ? { text, sessionId } : null;
124
+ }
81
125
  /** The error text of an `is_error` result event, if the stream has one. */
82
126
  export function messagesStreamFailure(stdout) {
83
127
  let reason;
@@ -85,6 +129,8 @@ export function messagesStreamFailure(stdout) {
85
129
  const ev = parseJsonLine(line);
86
130
  if (ev?.type === "result" && ev.is_error === true && typeof ev.result === "string" && ev.result.trim())
87
131
  reason = ev.result.trim();
132
+ if (ev?.type === "error" && typeof ev.message === "string" && ev.message.trim())
133
+ reason = ev.message.trim();
88
134
  }
89
135
  return reason;
90
136
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Provider account resolution (spec §5.5).
3
+ *
4
+ * Naming an account and omitting one mean different things: an explicit
5
+ * disabled id is refused; omitting rotates past a disabled default.
6
+ * Status reports `configured` and `logged_in` separately. logged_in is
7
+ * derived from whether <home>/auth.json EXISTS — this module never reads,
8
+ * prints, copies, or hashes that file.
9
+ */
10
+ import type { LaneProvider, LanesConfig } from "./types.js";
11
+ export declare const PROVIDER_HOME_ENV: Record<LaneProvider, string>;
12
+ export declare const AUTH_JSON_NAME = "auth.json";
13
+ export interface AccountDeps {
14
+ existsSync: (p: string) => boolean;
15
+ }
16
+ export interface ResolvedAccount {
17
+ provider: LaneProvider;
18
+ id: string;
19
+ home: string;
20
+ enabled: true;
21
+ envVar: string;
22
+ }
23
+ export interface AccountStatus {
24
+ provider: LaneProvider;
25
+ id: string;
26
+ home: string;
27
+ enabled: boolean;
28
+ disabled_reason?: string;
29
+ /** Present in personal/lanes/config.json. */
30
+ configured: boolean;
31
+ /**
32
+ * True iff `<home>/auth.json` exists. Never inspects the file contents.
33
+ */
34
+ logged_in: boolean;
35
+ envVar: string;
36
+ }
37
+ export declare function resolveAccount(provider: LaneProvider, requestedId: string | undefined, config: LanesConfig): ResolvedAccount;
38
+ export declare function authJsonPath(home: string): string;
39
+ export declare function accountStatus(provider: LaneProvider, accountId: string, config: LanesConfig, deps?: AccountDeps): AccountStatus;
40
+ export declare function listAccountStatuses(provider: LaneProvider, config: LanesConfig, deps?: AccountDeps): AccountStatus[];
41
+ /** Environment assignment the worker process should receive. */
42
+ export declare function accountEnv(resolved: ResolvedAccount): Record<string, string>;
43
+ //# sourceMappingURL=accounts.d.ts.map