@indigoai-us/hq-cli 5.114.0 → 5.115.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A company created from a local bot's conversation is owned by the person
3
+ * (the turn runs with the owner's sign-in), so the bot that helped create it
4
+ * is not in it. The bot passes its own id along (HQ_BOT_AGENT_UID — an id,
5
+ * never a credential), and `hq onboard create-company` adds that bot as a
6
+ * member right after, so it can keep helping inside the new company.
7
+ *
8
+ * Best effort: a failure here never fails the company that was just created.
9
+ */
10
+ import { inviteMember } from "./members.js";
11
+ export declare const HQ_BOT_AGENT_UID_ENV = "HQ_BOT_AGENT_UID";
12
+ /** The bot to add to `companyUid`, or null when this is not a bot's turn. */
13
+ export declare function botToAddAfterCreateCompany(env: NodeJS.ProcessEnv, companyUid: string | undefined): {
14
+ agentUid: string;
15
+ companyUid: string;
16
+ } | null;
17
+ export interface AddBotDeps {
18
+ invite: typeof inviteMember;
19
+ callerUid: (token: string) => Promise<string>;
20
+ }
21
+ /** Add the bot as a member. Returns a one-line outcome for the log; never throws. */
22
+ export declare function addBotToNewCompany(input: {
23
+ agentUid: string;
24
+ companyUid: string;
25
+ token: string;
26
+ }, deps?: AddBotDeps): Promise<{
27
+ ok: true;
28
+ } | {
29
+ ok: false;
30
+ reason: string;
31
+ }>;
32
+ //# sourceMappingURL=onboard-bot-membership.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A company created from a local bot's conversation is owned by the person
3
+ * (the turn runs with the owner's sign-in), so the bot that helped create it
4
+ * is not in it. The bot passes its own id along (HQ_BOT_AGENT_UID — an id,
5
+ * never a credential), and `hq onboard create-company` adds that bot as a
6
+ * member right after, so it can keep helping inside the new company.
7
+ *
8
+ * Best effort: a failure here never fails the company that was just created.
9
+ */
10
+ import { getCallerPersonUid, inviteMember } from "./members.js";
11
+ export const HQ_BOT_AGENT_UID_ENV = "HQ_BOT_AGENT_UID";
12
+ const AGENT_UID_RE = /^agt_[A-Za-z0-9]{1,64}$/;
13
+ /** The bot to add to `companyUid`, or null when this is not a bot's turn. */
14
+ export function botToAddAfterCreateCompany(env, companyUid) {
15
+ const agentUid = env[HQ_BOT_AGENT_UID_ENV]?.trim() ?? "";
16
+ const company = companyUid?.trim() ?? "";
17
+ if (!AGENT_UID_RE.test(agentUid) || !company.startsWith("cmp_"))
18
+ return null;
19
+ return { agentUid, companyUid: company };
20
+ }
21
+ const defaultDeps = { invite: inviteMember, callerUid: getCallerPersonUid };
22
+ /** Add the bot as a member. Returns a one-line outcome for the log; never throws. */
23
+ export async function addBotToNewCompany(input, deps = defaultDeps) {
24
+ try {
25
+ const callerUid = await deps.callerUid(input.token);
26
+ await deps.invite({
27
+ target: input.agentUid,
28
+ role: "member",
29
+ sendEmail: false,
30
+ companyUid: input.companyUid,
31
+ callerUid,
32
+ token: input.token,
33
+ });
34
+ return { ok: true };
35
+ }
36
+ catch (err) {
37
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
38
+ }
39
+ }
40
+ //# sourceMappingURL=onboard-bot-membership.js.map
@@ -25,6 +25,7 @@ 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 { addBotToNewCompany, botToAddAfterCreateCompany } from "./onboard-bot-membership.js";
28
29
  // ---------------------------------------------------------------------------
29
30
  // Command registration
30
31
  // ---------------------------------------------------------------------------
@@ -63,6 +64,15 @@ export function registerOnboardCommand(program) {
63
64
  console.error(chalk.red(`\n✗ Onboarding failed: ${result.error}`));
64
65
  process.exit(1);
65
66
  }
67
+ // Created from a local bot's conversation: keep that bot in the company.
68
+ const bot = botToAddAfterCreateCompany(process.env, result.result?.companyUid);
69
+ if (bot) {
70
+ const added = await addBotToNewCompany({ ...bot, token: accessToken });
71
+ if (added.ok)
72
+ console.log(chalk.green(`✓ Added your bot (${bot.agentUid}) to ${options.name} as a member`));
73
+ else
74
+ console.log(chalk.yellow(`! Could not add your bot (${bot.agentUid}) to ${options.name}: ${added.reason}. Invite it later with: hq members invite ${bot.agentUid} --company ${options.slug} --role member`));
75
+ }
66
76
  }
67
77
  catch (err) {
68
78
  console.error(chalk.red("\n✗ Error:"), err instanceof Error ? err.message : String(err));
@@ -45,6 +45,8 @@ export interface AgentRecord {
45
45
  ownerUid?: string | null;
46
46
  computeMode?: string;
47
47
  botKind?: string;
48
+ /** Company bots: the `cmp_` uids the server made it a member of. */
49
+ companyMemberships?: string[];
48
50
  online?: boolean;
49
51
  runtime?: {
50
52
  status?: string;
@@ -180,9 +182,18 @@ export declare class BotApi {
180
182
  promotion: BotPromotionState;
181
183
  upload: BotPromotionUpload;
182
184
  }>;
185
+ /**
186
+ * POST /v1/agents — a local bot identity. `kind` is the bot kind the cloud
187
+ * stores and enforces ("personal" acts as its owner through mirror-owner;
188
+ * "company" is evaluated as itself through its memberships). Default personal.
189
+ */
183
190
  createLocalBot(input: {
184
191
  name: string;
185
192
  slug?: string;
193
+ kind?: "personal" | "company";
194
+ /** Company bots: the company it acts for, plus any further companies. The server makes it a member of each. */
195
+ companyUid?: string;
196
+ companyMemberships?: string[];
186
197
  }): Promise<{
187
198
  agent: AgentRecord;
188
199
  identity: {
@@ -180,6 +180,11 @@ export class BotApi {
180
180
  async promotionUpload(agentUid, input) {
181
181
  return (await this.call(`/v1/agents/${encodeURIComponent(agentUid)}/promote/snapshot-upload`, { method: "POST", body: input })).body;
182
182
  }
183
+ /**
184
+ * POST /v1/agents — a local bot identity. `kind` is the bot kind the cloud
185
+ * stores and enforces ("personal" acts as its owner through mirror-owner;
186
+ * "company" is evaluated as itself through its memberships). Default personal.
187
+ */
183
188
  async createLocalBot(input) {
184
189
  const { body } = await this.call("/v1/agents", {
185
190
  method: "POST",
@@ -187,7 +192,9 @@ export class BotApi {
187
192
  name: input.name,
188
193
  ...(input.slug ? { slug: input.slug } : {}),
189
194
  computeMode: "local",
190
- botKind: "personal",
195
+ botKind: input.kind ?? "personal",
196
+ ...(input.companyUid ? { companyUid: input.companyUid } : {}),
197
+ ...(input.companyMemberships?.length ? { companyMemberships: input.companyMemberships } : {}),
191
198
  role: "local-bot",
192
199
  },
193
200
  });
@@ -4,6 +4,31 @@
4
4
  export declare const BOT_RUNTIMES: readonly ["claude", "codex", "grok"];
5
5
  export type BotRuntimeId = (typeof BOT_RUNTIMES)[number];
6
6
  export declare function isBotRuntimeId(value: unknown): value is BotRuntimeId;
7
+ /**
8
+ * What a bot is (owner decisions, 2026-09-15):
9
+ * - "personal": acts as its owner. Every turn (DMs, kickoff, rooms) runs with
10
+ * the owner's sign-in, so it reaches everything the owner can and can create
11
+ * companies, bots, invites and shares for them. Setup is a personal bot.
12
+ * - "company": acts as itself with its own identity, like a cloud agent. It is
13
+ * a member of one or more companies (`companies`) and reaches only theirs.
14
+ */
15
+ export declare const BOT_KINDS: readonly ["personal", "company"];
16
+ export type BotKind = (typeof BOT_KINDS)[number];
17
+ export declare function isBotKind(value: unknown): value is BotKind;
18
+ /** The id of HQ's bundled setup worker; a bot running it is always personal. */
19
+ export declare const SETUP_BOT_WORKER_ID = "setup";
20
+ /** Trim, validate and dedupe company slugs; throws an expected error on a bad one. */
21
+ export declare function normalizeBotCompanies(values: readonly string[] | undefined): string[];
22
+ /**
23
+ * The kind a bot runs as. A bot.json without a `kind` was created before
24
+ * kinds existed: every such bot's cloud row says botKind "personal", it acted
25
+ * as its owner, and it was never invited into a company — so it is personal,
26
+ * even one running a company worker (its `companySlug` is still the company
27
+ * bind of its turns, not a membership). Setup is always personal.
28
+ */
29
+ export declare function effectiveBotKind(config: Pick<BotConfig, "kind" | "workerId">): BotKind;
30
+ /** The companies a bot belongs to (empty for a personal bot: its owner's memberships apply). */
31
+ export declare function effectiveBotCompanies(config: Pick<BotConfig, "kind" | "companies" | "workerId">): string[];
7
32
  /** Longest `intro` (first-start hello DM) a bot may store. */
8
33
  export declare const BOT_INTRO_MAX_CHARS = 500;
9
34
  /** Longest `kickoff` (first-start model turn prompt) a bot may store. */
@@ -48,6 +73,14 @@ export interface BotConfig {
48
73
  /** The person (prs_) who created and owns the bot. */
49
74
  ownerUid: string;
50
75
  runtime: BotRuntimeId;
76
+ /**
77
+ * "personal" (acts as its owner everywhere) or "company" (acts as itself,
78
+ * member of `companies`). Missing on bots created before kinds existed:
79
+ * see effectiveBotKind for the rule readBotConfig applies.
80
+ */
81
+ kind?: BotKind;
82
+ /** Company bots only: slugs of the companies the bot is a member of. */
83
+ companies?: string[];
51
84
  /** Optional model override handed to the runtime CLI (unset = the CLI's own default). */
52
85
  model?: string;
53
86
  /** Thinking level for the runtime CLI (see BOT_EFFORT_LEVELS; unset = DEFAULT_BOT_EFFORT). */
@@ -95,6 +128,8 @@ export interface BotConfig {
95
128
  enabled: boolean;
96
129
  /** Set once the bot has introduced itself to the owner (US-008 intro). */
97
130
  introSentAt?: string;
131
+ /** The kickoff could not run because the coding tool needed a sign-in; it runs once that works. */
132
+ kickoffPendingSignIn?: boolean;
98
133
  }
99
134
  export declare function readBotConfig(dir: string): BotConfig | null;
100
135
  export declare function writeBotConfig(dir: string, config: BotConfig): void;
@@ -8,6 +8,62 @@ export const BOT_RUNTIMES = ["claude", "codex", "grok"];
8
8
  export function isBotRuntimeId(value) {
9
9
  return typeof value === "string" && BOT_RUNTIMES.includes(value);
10
10
  }
11
+ /**
12
+ * What a bot is (owner decisions, 2026-09-15):
13
+ * - "personal": acts as its owner. Every turn (DMs, kickoff, rooms) runs with
14
+ * the owner's sign-in, so it reaches everything the owner can and can create
15
+ * companies, bots, invites and shares for them. Setup is a personal bot.
16
+ * - "company": acts as itself with its own identity, like a cloud agent. It is
17
+ * a member of one or more companies (`companies`) and reaches only theirs.
18
+ */
19
+ export const BOT_KINDS = ["personal", "company"];
20
+ export function isBotKind(value) {
21
+ return typeof value === "string" && BOT_KINDS.includes(value);
22
+ }
23
+ /** The id of HQ's bundled setup worker; a bot running it is always personal. */
24
+ export const SETUP_BOT_WORKER_ID = "setup";
25
+ /** Company slugs are path segments and cloud lookups: keep them plain. */
26
+ const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
27
+ /** Trim, validate and dedupe company slugs; throws an expected error on a bad one. */
28
+ export function normalizeBotCompanies(values) {
29
+ const out = [];
30
+ for (const raw of values ?? []) {
31
+ const slug = (raw ?? "").trim();
32
+ if (!slug)
33
+ continue;
34
+ if (!COMPANY_SLUG_RE.test(slug) || slug === "personal") {
35
+ throw Object.assign(new Error(`"${slug}" is not a company slug (letters, digits, . _ -).`), { expected: true });
36
+ }
37
+ if (!out.includes(slug))
38
+ out.push(slug);
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * The kind a bot runs as. A bot.json without a `kind` was created before
44
+ * kinds existed: every such bot's cloud row says botKind "personal", it acted
45
+ * as its owner, and it was never invited into a company — so it is personal,
46
+ * even one running a company worker (its `companySlug` is still the company
47
+ * bind of its turns, not a membership). Setup is always personal.
48
+ */
49
+ export function effectiveBotKind(config) {
50
+ if (config.workerId === SETUP_BOT_WORKER_ID)
51
+ return "personal";
52
+ return isBotKind(config.kind) ? config.kind : "personal";
53
+ }
54
+ /** The companies a bot belongs to (empty for a personal bot: its owner's memberships apply). */
55
+ export function effectiveBotCompanies(config) {
56
+ if (effectiveBotKind(config) !== "company")
57
+ return [];
58
+ // Tolerant on read: a malformed slug is dropped, never a reason to lose the bot.
59
+ const listed = [];
60
+ for (const raw of config.companies ?? []) {
61
+ const slug = typeof raw === "string" ? raw.trim() : "";
62
+ if (slug && COMPANY_SLUG_RE.test(slug) && !listed.includes(slug))
63
+ listed.push(slug);
64
+ }
65
+ return listed;
66
+ }
11
67
  /** Longest `intro` (first-start hello DM) a bot may store. */
12
68
  export const BOT_INTRO_MAX_CHARS = 500;
13
69
  /** Longest `kickoff` (first-start model turn prompt) a bot may store. */
@@ -105,10 +161,21 @@ export function readBotConfig(dir) {
105
161
  }
106
162
  if (!isBotRuntimeId(raw.runtime))
107
163
  return null;
108
- const { intro: rawIntro, kickoff: rawKickoff, ...rest } = raw;
164
+ const { intro: rawIntro, kickoff: rawKickoff, kind: _kind, companies: _companies, ...rest } = raw;
109
165
  const intro = typeof rawIntro === "string" && rawIntro.trim() ? rawIntro : undefined;
110
166
  const kickoff = typeof rawKickoff === "string" && rawKickoff.trim() ? rawKickoff : undefined;
111
- return { ...rest, ...(intro ? { intro } : {}), ...(kickoff ? { kickoff } : {}), enabled: raw.enabled !== false };
167
+ // Older bot.json files carry no kind: they read (and, on the first patch,
168
+ // persist) as personal, so every reader sees a kind.
169
+ const kind = effectiveBotKind(raw);
170
+ const companies = effectiveBotCompanies(raw);
171
+ return {
172
+ ...rest,
173
+ kind,
174
+ ...(kind === "company" ? { companies } : {}),
175
+ ...(intro ? { intro } : {}),
176
+ ...(kickoff ? { kickoff } : {}),
177
+ enabled: raw.enabled !== false,
178
+ };
112
179
  }
113
180
  catch {
114
181
  return null;
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * Pure over an injected reader so tests never touch the real HQ.
18
18
  */
19
+ import type { BotKind } from "./config.js";
19
20
  export declare const MEMORY_FILE_BUDGET_BYTES: number;
20
21
  export declare const MEMORY_TOTAL_BUDGET_BYTES: number;
21
22
  export declare const CONTEXT_FILE_BUDGET_BYTES: number;
@@ -84,9 +85,20 @@ export interface BuildSystemPromptInput {
84
85
  workerSource?: "scaffold" | "worker";
85
86
  workerId?: string;
86
87
  companySlug?: string;
88
+ /** "personal" (acts as its owner everywhere) or "company" (acts as itself). Default personal. */
89
+ kind?: BotKind;
90
+ /** Company bots: the slugs of the companies the bot is a member of. */
91
+ companies?: string[];
87
92
  /** hqRoot-relative (default `${workerDir}/memory`) or absolute. */
88
93
  memoryDir?: string;
89
94
  }
95
+ /**
96
+ * One paragraph saying which kind of bot this is and what that means for the
97
+ * `hq` commands it runs. The owner's decision (2026-09-15): a personal bot
98
+ * acts as its owner everywhere; a company bot acts as itself and cannot
99
+ * create anything for the owner.
100
+ */
101
+ export declare function botKindParagraph(input: Pick<BuildSystemPromptInput, "agentUid" | "ownerUid" | "kind" | "companies">): string;
90
102
  /**
91
103
  * The standing instructions the bot runs under. The DM body is delivered as
92
104
  * the user turn; this text is the system prompt (claude/codex) or the prompt
@@ -94,7 +106,7 @@ export interface BuildSystemPromptInput {
94
106
  */
95
107
  export declare function buildSystemPrompt(input: BuildSystemPromptInput): string;
96
108
  /** Fixed text sent to any non-owner who DMs the bot (never reaches the model). */
97
- export declare function nonOwnerRefusalText(botName: string): string;
109
+ export declare function nonOwnerRefusalText(botName: string, kind?: BotKind): string;
98
110
  /**
99
111
  * The one-time introduction the bot DMs its owner when it first comes online.
100
112
  * A bot created with `--intro` sends that text verbatim instead.
@@ -229,6 +229,32 @@ export function readPromptSources(hqRoot, workerDir, io = defaultPromptFs, opts
229
229
  }
230
230
  return { workerYaml, persona: read("persona.md"), memory };
231
231
  }
232
+ /**
233
+ * One paragraph saying which kind of bot this is and what that means for the
234
+ * `hq` commands it runs. The owner's decision (2026-09-15): a personal bot
235
+ * acts as its owner everywhere; a company bot acts as itself and cannot
236
+ * create anything for the owner.
237
+ */
238
+ export function botKindParagraph(input) {
239
+ const kind = input.kind ?? "personal";
240
+ if (kind === "company") {
241
+ const list = (input.companies ?? []).filter((c) => c.trim());
242
+ const members = list.length > 0 ? list.map((c) => `\`${c}\``).join(", ") : "(none recorded yet)";
243
+ return (`You are a COMPANY bot: you act as yourself, ${input.agentUid}, with your own identity and your own permissions — never as your owner. ` +
244
+ `You are a member of these companies: ${members}. You can only reach those companies' files, memory, and secrets; ` +
245
+ `people in those companies can @mention you in their rooms, and your owner can message you directly, but you act as yourself there too. ` +
246
+ `Every \`hq\` command you run — in direct messages and in rooms — runs with your own sign-in: \`hq whoami\` and membership lookups describe you, the bot. ` +
247
+ `You cannot create a company, a bot, an invite or a share on your owner's behalf, and you cannot reach companies you are not a member of. ` +
248
+ `If asked to, say plainly that you are a company bot and cannot do that, and suggest your owner ask one of their personal bots (or do it themselves in HQ). ` +
249
+ `Nobody's messages carry an "Owner context" block; never state which companies your owner belongs to.`);
250
+ }
251
+ return (`You are a PERSONAL bot: you act as your owner, ${input.ownerUid}. ` +
252
+ `Every \`hq\` command you run — in direct messages, in rooms, and on your first task — runs with your owner's own sign-in, so it reaches everything they can: all of their companies and files. ` +
253
+ `Creating a company, a bot, an invite or a share for them works directly, so do it yourself when they ask — never tell them to open a terminal or run a command. ` +
254
+ `Direct messages from your owner start with an "Owner context" block checked with your owner's own sign-in; that is the only source for your owner's companies. ` +
255
+ `If it says the check failed, say you could not check; never say your owner has no company unless that block says so. ` +
256
+ `Room messages do not carry it, and other people read your answers there: in a room, never state which companies your owner belongs to, and never repeat anything from their files or your DMs.`);
257
+ }
232
258
  /**
233
259
  * The standing instructions the bot runs under. The DM body is delivered as
234
260
  * the user turn; this text is the system prompt (claude/codex) or the prompt
@@ -240,23 +266,19 @@ export function buildSystemPrompt(input) {
240
266
  const worker = input.sources.worker;
241
267
  const workerId = input.workerId ?? worker?.id ?? path.basename(input.workerDir);
242
268
  const company = input.companySlug ?? worker?.company;
269
+ const kind = input.kind ?? "personal";
243
270
  const parts = [];
244
271
  const opening = workerMode
245
- ? `You are "${input.botName}", a personal HQ bot running the company worker \`${workerId}\`${company ? ` for \`${company}\`` : ""}, locally on your owner's computer.` +
272
+ ? `You are "${input.botName}", a ${kind} HQ bot running the company worker \`${workerId}\`${company ? ` for \`${company}\`` : ""}, locally on your owner's computer.` +
246
273
  (worker?.name ? ` The worker is "${worker.name}".` : "") +
247
274
  (worker?.description ? ` ${worker.description}` : "") +
248
275
  `\nYou work only inside \`${company ?? "this company"}\`'s context: its files, policies, services, and credentials — never another company's.`
249
- : `You are "${input.botName}", a personal HQ bot that runs locally on your owner's computer.`;
276
+ : `You are "${input.botName}", a ${kind} HQ bot that runs locally on your owner's computer.`;
250
277
  parts.push(`${opening}\n` +
251
- `Your HQ identity is ${input.agentUid}. Your owner is ${input.ownerUid}; only your owner can message you.\n` +
252
- `You act with your owner's permissions, and every tool and command is pre-approved for you — do not ask for approval, just do the work. ` +
253
- `Their HQ folder is ${input.hqRoot}; it is your working directory, so its files, skills, and \`hq\` commands are available directly.\n` +
254
- `\`hq\` commands you run authenticate as you, the bot (${input.agentUid}), not as your owner: \`hq whoami\`, membership and company lookups describe the bot, ` +
255
- `and a bot usually belongs to no company. Never use them to say who your owner is or which companies they belong to. ` +
256
- `The exception is \`hq bot\` (create, list, rm, promote): it always acts as your owner, so you can create a bot for them when they ask; its owner is your owner. ` +
257
- `Direct messages from your owner start with an "Owner context" block checked with your owner's own sign-in; that is the only source for your owner's companies. ` +
258
- `If it says the check failed, say you could not check; never say your owner has no company unless that block says so. ` +
259
- `Room messages do not carry it: in a room, never state which companies your owner belongs to.\n` +
278
+ `Your HQ identity is ${input.agentUid}. Your owner is ${input.ownerUid}; only your owner can send you direct messages.\n` +
279
+ `${kind === "personal" ? "You act with your owner's permissions" : "You act with your own permissions"}, and every tool and command is pre-approved for you — do not ask for approval, just do the work. ` +
280
+ `${kind === "personal" ? "Their" : "Your owner's"} HQ folder is ${input.hqRoot}; it is your working directory, so its files, skills, and \`hq\` commands are available directly.\n` +
281
+ `${botKindParagraph(input)}\n` +
260
282
  `Each message you receive is a direct message from your owner. Whatever you print as your final answer is sent back as a DM reply, ` +
261
283
  `so answer the person directly, in plain language, and keep it as short as the request allows. ` +
262
284
  `Do not narrate tool use or repeat the question. If you cannot do something, say so and suggest the nearest thing you can do.\n` +
@@ -311,7 +333,10 @@ export function buildSystemPrompt(input) {
311
333
  return `${parts.join("\n\n")}\n`;
312
334
  }
313
335
  /** Fixed text sent to any non-owner who DMs the bot (never reaches the model). */
314
- export function nonOwnerRefusalText(botName) {
336
+ export function nonOwnerRefusalText(botName, kind = "personal") {
337
+ if (kind === "company") {
338
+ return `Hi — I'm ${botName}, a company bot, and I only take direct messages from my owner. You can @mention me in one of my companies' rooms.`;
339
+ }
315
340
  return `Hi — I'm ${botName}, a personal bot, and I only take messages from my owner. Please reach out to them directly.`;
316
341
  }
317
342
  /**
@@ -26,6 +26,11 @@
26
26
  * started (error, crash, owner stop, restart) posts one "did not finish"
27
27
  * message and is NOT run again — it may already have done outward things.
28
28
  * inflight.json marks the turn in progress so a restart can say so.
29
+ * - a turn that fails because the coding tool's sign-in no longer works
30
+ * (runtime-sign-in.ts) is not a failure of the message: the bot says once
31
+ * per conversation that it needs a sign-in, keeps the message un-acked,
32
+ * holds new turns, and tries again every SIGN_IN_RETRY_MS (and at once
33
+ * after a restart). A turn that works clears it.
29
34
  * - failures before the model starts (missing binary) retry with backoff;
30
35
  * 5 model failures in 10 minutes → state `failed` and a clean exit (launchd
31
36
  * does not respawn a clean exit). Owner stops never count.
@@ -34,7 +39,7 @@
34
39
  */
35
40
  import { type OwnerContext } from "./owner-context.js";
36
41
  import type { BotApi, InboxItem } from "./api.js";
37
- import type { BotConfig } from "./config.js";
42
+ import type { BotConfig, BotKind } from "./config.js";
38
43
  import { type ProgressPosterOptions } from "./progress.js";
39
44
  import { type BotLogger } from "./log.js";
40
45
  import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult } from "./runtime/index.js";
@@ -79,15 +84,37 @@ export interface BotRunDeps {
79
84
  progress?: Pick<ProgressPosterOptions, "coalesceMs" | "minGapMs" | "workingNoticeMs" | "setTimer" | "clearTimer" | "now">;
80
85
  /**
81
86
  * The owner's verified company context, prepended to every owner DM turn
82
- * (never room turns) so the model never mistakes the bot's own (empty)
83
- * memberships for its owner's.
87
+ * of a PERSONAL bot (never room turns, never a company bot's turns) so the
88
+ * model never mistakes the bot's own (empty) memberships for its owner's.
84
89
  * Omitted in tests that do not exercise it.
85
90
  */
86
91
  ownerContext?: () => Promise<OwnerContext>;
87
92
  /** Turns run at once across conversations (default MAX_PARALLEL_TURNS). */
88
93
  maxParallelTurns?: number;
94
+ /** Wait between retries while the coding tool needs a sign-in (default SIGN_IN_RETRY_MS). */
95
+ signInRetryMs?: number;
89
96
  }
90
97
  export declare const KICKOFF_MESSAGE_ID = "kickoff";
98
+ /**
99
+ * The environment a model turn's `hq` commands run in.
100
+ *
101
+ * The bot process carries its own machine identity in HQ_MACHINE_CREDS_FILE /
102
+ * HQ_MACHINE_TOKEN_STATE_DIR so its inbox, heartbeat and posts are the bot's.
103
+ * What a turn inherits depends on the bot's kind:
104
+ * - A PERSONAL bot acts as its owner on every turn (DMs, the kickoff, rooms):
105
+ * everything the owner asks for — a company, a bot, an invite, a share —
106
+ * belongs to the owner's account, and the cloud rightly refuses a bot that
107
+ * tries to own it ("No person entity found"). Stripping the marker makes
108
+ * every `hq` command in the turn use the owner's sign-in, the same one the
109
+ * desktop app shares, so no command needs a special case. The bot's id
110
+ * (never a credential) rides along in HQ_BOT_AGENT_UID so what the owner
111
+ * creates in the turn — a company — can add the bot as a member afterwards.
112
+ * - A COMPANY bot acts as itself on every turn, like a cloud agent: the
113
+ * machine identity stays, and the cloud evaluates it through its own
114
+ * company memberships. It never acts as the owner, in DMs or in rooms.
115
+ * Pure so the rule is unit-testable.
116
+ */
117
+ export declare function turnEnv(base: NodeJS.ProcessEnv, kind: BotKind, agentUid?: string): NodeJS.ProcessEnv;
91
118
  /** The core worker HQ's setup bot runs. */
92
119
  export declare const SETUP_WORKER_ID = "setup";
93
120
  /** The one message a person gets when a turn on their message did not finish. */