@indigoai-us/hq-cli 5.109.16 → 5.111.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 (82) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/assets/bot-workers/setup/context/USER-GUIDE.md +363 -0
  3. package/assets/bot-workers/setup/context/quick-reference.md +199 -0
  4. package/assets/bot-workers/setup/skills/first-company.md +71 -0
  5. package/assets/bot-workers/setup/skills/standing-help.md +74 -0
  6. package/assets/bot-workers/setup/worker.yaml +422 -0
  7. package/dist/commands/bot-continuity.d.ts +28 -0
  8. package/dist/commands/bot-continuity.js +68 -0
  9. package/dist/commands/bot.d.ts +73 -0
  10. package/dist/commands/bot.js +776 -0
  11. package/dist/commands/skill.d.ts +12 -3
  12. package/dist/commands/skill.js +79 -3
  13. package/dist/commands/workers.d.ts +2 -14
  14. package/dist/commands/workers.js +2 -8
  15. package/dist/lib/bot/api.d.ts +202 -0
  16. package/dist/lib/bot/api.js +202 -0
  17. package/dist/lib/bot/company-bind.d.ts +27 -0
  18. package/dist/lib/bot/company-bind.js +62 -0
  19. package/dist/lib/bot/config.d.ts +106 -0
  20. package/dist/lib/bot/config.js +141 -0
  21. package/dist/lib/bot/continuity-download.d.ts +28 -0
  22. package/dist/lib/bot/continuity-download.js +75 -0
  23. package/dist/lib/bot/continuity-install.d.ts +14 -0
  24. package/dist/lib/bot/continuity-install.js +101 -0
  25. package/dist/lib/bot/continuity.d.ts +66 -0
  26. package/dist/lib/bot/continuity.js +301 -0
  27. package/dist/lib/bot/creds.d.ts +24 -0
  28. package/dist/lib/bot/creds.js +51 -0
  29. package/dist/lib/bot/daemon.d.ts +75 -0
  30. package/dist/lib/bot/daemon.js +316 -0
  31. package/dist/lib/bot/inbox-state.d.ts +18 -0
  32. package/dist/lib/bot/inbox-state.js +51 -0
  33. package/dist/lib/bot/index.d.ts +16 -0
  34. package/dist/lib/bot/index.js +16 -0
  35. package/dist/lib/bot/inflight.d.ts +40 -0
  36. package/dist/lib/bot/inflight.js +44 -0
  37. package/dist/lib/bot/log.d.ts +13 -0
  38. package/dist/lib/bot/log.js +59 -0
  39. package/dist/lib/bot/owner-context.d.ts +75 -0
  40. package/dist/lib/bot/owner-context.js +151 -0
  41. package/dist/lib/bot/paths.d.ts +61 -0
  42. package/dist/lib/bot/paths.js +103 -0
  43. package/dist/lib/bot/progress.d.ts +84 -0
  44. package/dist/lib/bot/progress.js +167 -0
  45. package/dist/lib/bot/promote.d.ts +16 -0
  46. package/dist/lib/bot/promote.js +106 -0
  47. package/dist/lib/bot/promotion-hold.d.ts +24 -0
  48. package/dist/lib/bot/promotion-hold.js +103 -0
  49. package/dist/lib/bot/promotion-receipt.d.ts +9 -0
  50. package/dist/lib/bot/promotion-receipt.js +56 -0
  51. package/dist/lib/bot/promotion-upload.d.ts +16 -0
  52. package/dist/lib/bot/promotion-upload.js +65 -0
  53. package/dist/lib/bot/prompt.d.ts +103 -0
  54. package/dist/lib/bot/prompt.js +329 -0
  55. package/dist/lib/bot/room-policy.d.ts +53 -0
  56. package/dist/lib/bot/room-policy.js +73 -0
  57. package/dist/lib/bot/run.d.ts +98 -0
  58. package/dist/lib/bot/run.js +787 -0
  59. package/dist/lib/bot/runtime/claude.d.ts +49 -0
  60. package/dist/lib/bot/runtime/claude.js +151 -0
  61. package/dist/lib/bot/runtime/codex.d.ts +28 -0
  62. package/dist/lib/bot/runtime/codex.js +147 -0
  63. package/dist/lib/bot/runtime/grok.d.ts +16 -0
  64. package/dist/lib/bot/runtime/grok.js +67 -0
  65. package/dist/lib/bot/runtime/index.d.ts +35 -0
  66. package/dist/lib/bot/runtime/index.js +279 -0
  67. package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
  68. package/dist/lib/bot/runtime/messages-stream.js +85 -0
  69. package/dist/lib/bot/runtime/types.d.ts +136 -0
  70. package/dist/lib/bot/runtime/types.js +51 -0
  71. package/dist/lib/bot/scaffold.d.ts +38 -0
  72. package/dist/lib/bot/scaffold.js +94 -0
  73. package/dist/lib/bot/session.d.ts +19 -0
  74. package/dist/lib/bot/session.js +39 -0
  75. package/dist/lib/bot/status.d.ts +40 -0
  76. package/dist/lib/bot/status.js +66 -0
  77. package/dist/lib/bot/worker-source.d.ts +66 -0
  78. package/dist/lib/bot/worker-source.js +283 -0
  79. package/dist/lib/workers-registry/read.d.ts +15 -0
  80. package/dist/lib/workers-registry/read.js +17 -0
  81. package/dist/register-all.js +2 -0
  82. package/package.json +2 -1
@@ -0,0 +1,279 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { createClaudeRuntime } from "./claude.js";
7
+ import { createCodexRuntime } from "./codex.js";
8
+ import { createGrokRuntime } from "./grok.js";
9
+ import { parseJsonLine, RuntimeError, turnTimeoutMs, stderrExcerpt, } from "./types.js";
10
+ export * from "./types.js";
11
+ export { createClaudeRuntime, claudePermissionMode } from "./claude.js";
12
+ export { createCodexRuntime, codexPrompt } from "./codex.js";
13
+ export { createGrokRuntime, grokPrompt } from "./grok.js";
14
+ export function runtimeFor(id, env = process.env) {
15
+ if (id === "claude")
16
+ return createClaudeRuntime(env);
17
+ if (id === "codex")
18
+ return createCodexRuntime();
19
+ return createGrokRuntime();
20
+ }
21
+ /**
22
+ * Real child spawn, stdin closed. stdout is delivered line by line while the
23
+ * process runs (for streaming runtimes) and also returned whole at the end.
24
+ * No time limit unless `timeoutMs` is set; `signal` kills the process.
25
+ */
26
+ /**
27
+ * Every live descendant of `pid` (children first found, deepest last). The
28
+ * model CLI starts its own tools and shells; stopping only the top process
29
+ * leaves those running and holding the output pipe open, so a stop would hang
30
+ * until they finish. POSIX only; empty when `ps` is unavailable.
31
+ */
32
+ export function descendantPids(pid, listProcesses = defaultListProcesses) {
33
+ let table;
34
+ try {
35
+ table = listProcesses();
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ const children = new Map();
41
+ for (const row of table.split("\n")) {
42
+ const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(row);
43
+ if (!m)
44
+ continue;
45
+ const child = Number(m[1]);
46
+ const parent = Number(m[2]);
47
+ const list = children.get(parent) ?? [];
48
+ list.push(child);
49
+ children.set(parent, list);
50
+ }
51
+ const out = [];
52
+ const queue = [...(children.get(pid) ?? [])];
53
+ const seen = new Set([pid]);
54
+ while (queue.length > 0) {
55
+ const next = queue.shift();
56
+ if (seen.has(next))
57
+ continue;
58
+ seen.add(next);
59
+ out.push(next);
60
+ queue.push(...(children.get(next) ?? []));
61
+ }
62
+ return out;
63
+ }
64
+ function defaultListProcesses() {
65
+ return execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8", timeout: 5_000 });
66
+ }
67
+ /** After a stop, wait this long for the output pipes to close before settling anyway. */
68
+ export const STOP_SETTLE_GRACE_MS = 2_000;
69
+ export const defaultSpawn = (command, args, options) => new Promise((resolve, reject) => {
70
+ let stdout = "";
71
+ let stderr = "";
72
+ let partial = "";
73
+ let settled = false;
74
+ const child = spawn(command, args, {
75
+ cwd: options.cwd,
76
+ env: options.env,
77
+ stdio: ["ignore", "pipe", "pipe"],
78
+ });
79
+ let stopping = false;
80
+ const signalTree = (sig, pids) => {
81
+ for (const p of pids) {
82
+ try {
83
+ process.kill(p, sig);
84
+ }
85
+ catch {
86
+ /* already gone */
87
+ }
88
+ }
89
+ };
90
+ const kill = () => {
91
+ stopping = true;
92
+ // Snapshot the tree before the parent dies (its children get re-parented).
93
+ const tree = process.platform === "win32" || child.pid === undefined ? [] : descendantPids(child.pid);
94
+ try {
95
+ child.kill("SIGTERM");
96
+ }
97
+ catch {
98
+ /* ignore */
99
+ }
100
+ signalTree("SIGTERM", tree);
101
+ // A CLI (or a tool it started) that ignores SIGTERM still goes.
102
+ setTimeout(() => {
103
+ try {
104
+ if (child.exitCode === null && child.signalCode === null)
105
+ child.kill("SIGKILL");
106
+ }
107
+ catch {
108
+ /* ignore */
109
+ }
110
+ signalTree("SIGKILL", tree);
111
+ }, 5_000).unref?.();
112
+ };
113
+ const timer = options.timeoutMs !== undefined
114
+ ? setTimeout(() => {
115
+ if (settled)
116
+ return;
117
+ stderr += `\n[hq bot] runtime turn exceeded ${options.timeoutMs}ms (HQ_BOT_TURN_TIMEOUT_MS) and was stopped`;
118
+ kill();
119
+ }, options.timeoutMs)
120
+ : null;
121
+ const onAbort = () => {
122
+ if (!settled)
123
+ kill();
124
+ };
125
+ if (options.signal) {
126
+ if (options.signal.aborted)
127
+ onAbort();
128
+ else
129
+ options.signal.addEventListener("abort", onAbort, { once: true });
130
+ }
131
+ const emit = (line) => {
132
+ if (!options.onStdoutLine || !line.trim())
133
+ return;
134
+ try {
135
+ options.onStdoutLine(line);
136
+ }
137
+ catch {
138
+ /* a listener must never break the turn */
139
+ }
140
+ };
141
+ const cleanup = () => {
142
+ if (timer)
143
+ clearTimeout(timer);
144
+ options.signal?.removeEventListener("abort", onAbort);
145
+ };
146
+ child.stdout?.on("data", (d) => {
147
+ const chunk = d.toString();
148
+ stdout += chunk;
149
+ if (!options.onStdoutLine)
150
+ return;
151
+ partial += chunk;
152
+ let nl = partial.indexOf("\n");
153
+ while (nl >= 0) {
154
+ emit(partial.slice(0, nl));
155
+ partial = partial.slice(nl + 1);
156
+ nl = partial.indexOf("\n");
157
+ }
158
+ });
159
+ child.stderr?.on("data", (d) => {
160
+ stderr += d.toString();
161
+ });
162
+ child.on("error", (err) => {
163
+ if (settled)
164
+ return;
165
+ settled = true;
166
+ cleanup();
167
+ reject(err);
168
+ });
169
+ // A stopped process can exit while something it started (and we could not
170
+ // reach) still holds stdout open; settle shortly after the exit instead of
171
+ // waiting for that pipe.
172
+ child.on("exit", (code, signal) => {
173
+ if (!stopping)
174
+ return;
175
+ setTimeout(() => {
176
+ if (settled)
177
+ return;
178
+ settled = true;
179
+ cleanup();
180
+ child.stdout?.destroy();
181
+ child.stderr?.destroy();
182
+ if (partial)
183
+ emit(partial);
184
+ resolve({ code, signal, stdout, stderr });
185
+ }, STOP_SETTLE_GRACE_MS).unref?.();
186
+ });
187
+ child.on("close", (code, signal) => {
188
+ if (settled)
189
+ return;
190
+ settled = true;
191
+ cleanup();
192
+ if (partial)
193
+ emit(partial);
194
+ resolve({ code, signal, stdout, stderr });
195
+ });
196
+ });
197
+ /**
198
+ * Run one turn: spawn the runtime CLI, parse the reply, clean up scratch.
199
+ * Throws RuntimeError (with a bounded stderr excerpt) on any failure.
200
+ */
201
+ export async function runRuntimeTurn(runtime, input, deps = {}) {
202
+ const spawnImpl = deps.spawnImpl ?? defaultSpawn;
203
+ const scratchDir = deps.scratchDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "hq-bot-turn-"));
204
+ const outputFile = path.join(scratchDir, "last-message.txt");
205
+ const newSessionId = input.newSessionId ?? (deps.newSessionId ?? randomUUID)();
206
+ const args = runtime.buildArgs(input, { outputFile, newSessionId });
207
+ const env = input.env ?? deps.env ?? process.env;
208
+ const binary = runtime.resolveBinary?.(env) ?? runtime.binary;
209
+ const onEvent = input.onEvent;
210
+ const onStdoutLine = onEvent && runtime.parseStreamLine
211
+ ? (line) => {
212
+ for (const event of runtime.parseStreamLine(line)) {
213
+ try {
214
+ onEvent(event);
215
+ }
216
+ catch {
217
+ /* a listener must never break the turn */
218
+ }
219
+ }
220
+ }
221
+ : undefined;
222
+ const timeoutMs = deps.timeoutMs ?? turnTimeoutMs(env);
223
+ let result;
224
+ 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
+ });
232
+ }
233
+ catch (err) {
234
+ const code = err.code;
235
+ const msg = code === "ENOENT"
236
+ ? `${runtime.binary} is not installed or not on PATH`
237
+ : `failed to start ${runtime.binary}: ${err instanceof Error ? err.message : String(err)}`;
238
+ throw new RuntimeError(msg, msg, null, { started: false });
239
+ }
240
+ if (input.signal?.aborted) {
241
+ throw new RuntimeError(`${runtime.binary} was stopped`, "stopped by the owner", result.code, { aborted: true });
242
+ }
243
+ let outputFileText;
244
+ try {
245
+ if (fs.existsSync(outputFile))
246
+ outputFileText = fs.readFileSync(outputFile, "utf8");
247
+ }
248
+ catch {
249
+ /* ignore */
250
+ }
251
+ finally {
252
+ if (!deps.scratchDir) {
253
+ try {
254
+ fs.rmSync(scratchDir, { recursive: true, force: true });
255
+ }
256
+ catch {
257
+ /* ignore */
258
+ }
259
+ }
260
+ }
261
+ if (result.code !== 0 || result.signal) {
262
+ const reason = runtime.failureReason?.(result.stdout, result.stderr);
263
+ // Streaming runtimes write JSON events to stdout: never show those to a
264
+ // person as the reason. stderr, then a plain description, instead.
265
+ const stdoutIsEvents = result.stdout.split("\n").some((line) => parseJsonLine(line) !== null);
266
+ const fallback = result.stderr.trim()
267
+ ? stderrExcerpt(result.stderr)
268
+ : result.stdout.trim() && !stdoutIsEvents
269
+ ? stderrExcerpt(result.stdout)
270
+ : result.signal
271
+ ? `the ${runtime.binary} process was killed (${result.signal})`
272
+ : `${runtime.binary} exited with code ${result.code}`;
273
+ throw new RuntimeError(`${runtime.binary} exited with ${result.code ?? `signal ${result.signal}`}${reason ? `: ${reason.slice(0, 200)}` : ""}`, reason ? reason.slice(0, 400) : fallback, result.code);
274
+ }
275
+ const parsed = runtime.parseOutput(result.stdout, { outputFile, outputFileText });
276
+ // A resumed session keeps its id even when the CLI omits it from the envelope.
277
+ return { text: parsed.text, sessionId: parsed.sessionId ?? input.sessionId };
278
+ }
279
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The NDJSON stream Claude Code (`--output-format stream-json --verbose`) and
3
+ * Grok (`--output-format streaming-messages-json`) both write: Anthropic
4
+ * Messages wire shapes, one event per line.
5
+ *
6
+ * {"type":"system","subtype":"init","session_id":…}
7
+ * {"type":"assistant","message":{"content":[{"type":"text","text":…},
8
+ * {"type":"tool_use","name":…},{"type":"thinking",…}]},"parent_tool_use_id":null,"session_id":…}
9
+ * {"type":"user","message":{"content":[{"type":"tool_result",…}]}}
10
+ * {"type":"result","subtype":"success","is_error":false,"result":"<last message>","session_id":…}
11
+ *
12
+ * Recorded from Claude Code 2.1.258 and grok (model grok-4.6) on 2026-09-12;
13
+ * fixtures in runtime/fixtures/. Claude sends one content block per assistant
14
+ * event; Grok sends a whole message (thinking + text + tool_use) per event.
15
+ */
16
+ import { type RuntimeStreamEvent, type RuntimeTurnResult } from "./types.js";
17
+ /** Progress events in one stream line: top-level assistant text and tool starts only. */
18
+ export declare function messagesStreamEvents(line: string): RuntimeStreamEvent[];
19
+ /**
20
+ * The turn's result from the whole stream. Prefers the `result` event; also
21
+ * accepts the older single-object `--output-format json` envelope. Throws a
22
+ * RuntimeError for an error result, or when there is nothing usable.
23
+ */
24
+ export declare function messagesStreamResult(stdout: string, runtimeName: string): RuntimeTurnResult;
25
+ /** The error text of an `is_error` result event, if the stream has one. */
26
+ export declare function messagesStreamFailure(stdout: string): string | undefined;
27
+ //# sourceMappingURL=messages-stream.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The NDJSON stream Claude Code (`--output-format stream-json --verbose`) and
3
+ * Grok (`--output-format streaming-messages-json`) both write: Anthropic
4
+ * Messages wire shapes, one event per line.
5
+ *
6
+ * {"type":"system","subtype":"init","session_id":…}
7
+ * {"type":"assistant","message":{"content":[{"type":"text","text":…},
8
+ * {"type":"tool_use","name":…},{"type":"thinking",…}]},"parent_tool_use_id":null,"session_id":…}
9
+ * {"type":"user","message":{"content":[{"type":"tool_result",…}]}}
10
+ * {"type":"result","subtype":"success","is_error":false,"result":"<last message>","session_id":…}
11
+ *
12
+ * Recorded from Claude Code 2.1.258 and grok (model grok-4.6) on 2026-09-12;
13
+ * fixtures in runtime/fixtures/. Claude sends one content block per assistant
14
+ * event; Grok sends a whole message (thinking + text + tool_use) per event.
15
+ */
16
+ import { parseJsonLine, RuntimeError } from "./types.js";
17
+ /** Progress events in one stream line: top-level assistant text and tool starts only. */
18
+ export function messagesStreamEvents(line) {
19
+ const ev = parseJsonLine(line);
20
+ if (!ev || ev.type !== "assistant")
21
+ return [];
22
+ // Sub-agent traffic is the sub-agent's business, not a message to the person.
23
+ if (ev.parent_tool_use_id !== null && ev.parent_tool_use_id !== undefined)
24
+ return [];
25
+ const message = ev.message && typeof ev.message === "object" ? ev.message : null;
26
+ const content = Array.isArray(message?.content) ? message.content : [];
27
+ const out = [];
28
+ for (const block of content) {
29
+ if (!block || typeof block !== "object")
30
+ continue;
31
+ const b = block;
32
+ if (b.type === "text" && typeof b.text === "string" && b.text.trim())
33
+ out.push({ kind: "message", text: b.text.trim() });
34
+ else if (b.type === "tool_use" && typeof b.name === "string")
35
+ out.push({ kind: "tool", name: b.name });
36
+ }
37
+ return out;
38
+ }
39
+ /**
40
+ * The turn's result from the whole stream. Prefers the `result` event; also
41
+ * accepts the older single-object `--output-format json` envelope. Throws a
42
+ * RuntimeError for an error result, or when there is nothing usable.
43
+ */
44
+ export function messagesStreamResult(stdout, runtimeName) {
45
+ let result = null;
46
+ let sessionId;
47
+ let lastText;
48
+ let sawJson = false;
49
+ for (const line of stdout.split("\n")) {
50
+ const ev = parseJsonLine(line);
51
+ if (!ev)
52
+ continue;
53
+ sawJson = true;
54
+ if (typeof ev.session_id === "string")
55
+ sessionId = ev.session_id;
56
+ if (ev.type === "result" || (ev.type === undefined && ("result" in ev || "is_error" in ev)))
57
+ result = ev;
58
+ else if (ev.type === "assistant") {
59
+ const texts = messagesStreamEvents(line).flatMap((e) => (e.kind === "message" ? [e.text] : []));
60
+ if (texts.length > 0)
61
+ lastText = texts.join("\n\n");
62
+ }
63
+ }
64
+ if (!sawJson)
65
+ throw new RuntimeError(`${runtimeName} returned non-JSON output`, stdout.trim().slice(-400), null);
66
+ if (result?.is_error === true) {
67
+ const detail = typeof result.result === "string" && result.result ? result.result : String(result.subtype ?? "error");
68
+ throw new RuntimeError(`${runtimeName} reported an error: ${detail.slice(0, 200)}`, detail.slice(0, 400), null);
69
+ }
70
+ const text = (typeof result?.result === "string" && result.result.trim() ? result.result : lastText) ?? "";
71
+ if (typeof result?.session_id === "string")
72
+ sessionId = result.session_id;
73
+ return { text, sessionId };
74
+ }
75
+ /** The error text of an `is_error` result event, if the stream has one. */
76
+ export function messagesStreamFailure(stdout) {
77
+ let reason;
78
+ for (const line of stdout.split("\n")) {
79
+ const ev = parseJsonLine(line);
80
+ if (ev?.type === "result" && ev.is_error === true && typeof ev.result === "string" && ev.result.trim())
81
+ reason = ev.result.trim();
82
+ }
83
+ return reason;
84
+ }
85
+ //# sourceMappingURL=messages-stream.js.map
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Model-CLI runtime adapters (local-bots US-003).
3
+ *
4
+ * One headless invocation per inbound DM, resumed by the CLI's own session id
5
+ * so context carries across turns and across restarts. The CLI runs under the
6
+ * OWNER's login (claude / codex / grok) — no Indigo-billed model keys.
7
+ */
8
+ export interface RuntimeTurnInput {
9
+ /** The owner's DM text (the user turn). */
10
+ prompt: string;
11
+ /** Absolute path of the rendered system prompt file. */
12
+ systemPromptFile: string;
13
+ /** The system prompt text (for runtimes that cannot take a file). */
14
+ systemPrompt: string;
15
+ /** Working directory — the owner's HQ root, so skills and `hq` work. */
16
+ cwd: string;
17
+ /** Pre-approve every tool/command (headless bots cannot prompt). Default true. */
18
+ autoApprove?: boolean;
19
+ /** Claude only: explicit permission mode (a remembered fallback) — beats autoApprove. */
20
+ permissionMode?: string;
21
+ /** The owner's HQ root: runtimes with a write sandbox must allow it too,
22
+ * or the bot cannot edit its own memory notes under personal/workers/. */
23
+ hqRoot?: string;
24
+ /** Previous session id to resume, if any. */
25
+ sessionId?: string;
26
+ /** Optional model override. */
27
+ model?: string;
28
+ /** Thinking level for the CLI (runtimes fall back to medium). */
29
+ effort?: string;
30
+ /**
31
+ * The session id to start a NEW session under (ignored when resuming).
32
+ * Set by the run loop when the bot is company-bound so the hooks' session
33
+ * meta file and the CLI session share one id.
34
+ */
35
+ newSessionId?: string;
36
+ /** Environment for the runtime process (default: process.env). */
37
+ env?: NodeJS.ProcessEnv;
38
+ /**
39
+ * Called for each event parsed from the runtime's stdout WHILE the turn runs
40
+ * (a finished assistant message, a tool call). Exceptions are swallowed.
41
+ */
42
+ onEvent?: (event: RuntimeStreamEvent) => void;
43
+ /** Aborting kills the runtime process (owner stop); the turn then fails with `aborted`. */
44
+ signal?: AbortSignal;
45
+ }
46
+ /**
47
+ * What a runtime's streaming output means to the bot, per stdout line:
48
+ * - `message`: one whole assistant message worth posting as progress
49
+ * - `tool`: the model started a tool (name only — never arguments or results)
50
+ * Reasoning/thinking content, token deltas, tool payloads and tool results are
51
+ * never surfaced.
52
+ */
53
+ export type RuntimeStreamEvent = {
54
+ kind: "message";
55
+ text: string;
56
+ } | {
57
+ kind: "tool";
58
+ name: string;
59
+ };
60
+ export interface RuntimeTurnResult {
61
+ text: string;
62
+ sessionId?: string;
63
+ }
64
+ export interface SpawnResult {
65
+ code: number | null;
66
+ signal: NodeJS.Signals | null;
67
+ stdout: string;
68
+ stderr: string;
69
+ }
70
+ export interface SpawnOptions {
71
+ cwd: string;
72
+ env: NodeJS.ProcessEnv;
73
+ /** Kill the process after this long. Undefined = no limit (the default). */
74
+ timeoutMs?: number;
75
+ /** Each complete stdout line as it arrives (the trailing partial line on close). */
76
+ onStdoutLine?: (line: string) => void;
77
+ /** Kill the process when aborted. */
78
+ signal?: AbortSignal;
79
+ }
80
+ export interface SpawnLike {
81
+ (command: string, args: string[], options: SpawnOptions): Promise<SpawnResult>;
82
+ }
83
+ export declare class RuntimeError extends Error {
84
+ readonly stderrExcerpt: string;
85
+ readonly exitCode: number | null;
86
+ /** False when the runtime process never started (missing binary, spawn error): safe to retry. */
87
+ readonly started: boolean;
88
+ /** True when the turn was ended on purpose (owner stop), not by a model failure. */
89
+ readonly aborted: boolean;
90
+ constructor(message: string, stderrExcerpt: string, exitCode: number | null, opts?: {
91
+ started?: boolean;
92
+ aborted?: boolean;
93
+ });
94
+ }
95
+ export interface BotRuntime {
96
+ readonly id: "claude" | "codex" | "grok";
97
+ /** Executable name looked up on PATH (or an absolute path). */
98
+ readonly binary: string;
99
+ /**
100
+ * Optional: the executable to spawn for this turn when `binary` is not on
101
+ * PATH (e.g. Claude Code that only exists inside the Claude desktop app).
102
+ * Resolved per turn so an app update that swaps the version folder is picked up.
103
+ */
104
+ resolveBinary?(env: NodeJS.ProcessEnv): string;
105
+ /** Build the argv for one turn. Pure. */
106
+ buildArgs(input: RuntimeTurnInput, scratch: {
107
+ outputFile: string;
108
+ newSessionId: string;
109
+ }): string[];
110
+ /** Parse the CLI's stdout (and optional output file) into the reply. Pure. */
111
+ parseOutput(stdout: string, extra: {
112
+ outputFile?: string;
113
+ outputFileText?: string;
114
+ }): RuntimeTurnResult;
115
+ /** Optional: events in ONE stdout line of the streaming output format. Pure. */
116
+ parseStreamLine?(line: string): RuntimeStreamEvent[];
117
+ /**
118
+ * Optional: the human-readable reason for a failed turn (non-zero exit),
119
+ * mined from stdout/stderr — e.g. the API's "usage limit" or "model
120
+ * requires a newer CLI" message — so the owner sees the real cause instead
121
+ * of the CLI's last stderr chatter. Return undefined when nothing better
122
+ * than the stderr excerpt is known. Pure.
123
+ */
124
+ failureReason?(stdout: string, stderr: string): string | undefined;
125
+ }
126
+ /**
127
+ * Turns have no time limit: a turn runs until the model process exits (the
128
+ * owner can still stop the bot). `HQ_BOT_TURN_TIMEOUT_MS` is an escape hatch;
129
+ * unset, empty, zero or invalid means unlimited.
130
+ */
131
+ export declare const TURN_TIMEOUT_ENV = "HQ_BOT_TURN_TIMEOUT_MS";
132
+ export declare function turnTimeoutMs(env?: NodeJS.ProcessEnv): number | undefined;
133
+ /** Parse one NDJSON line; null when it is not a JSON object. */
134
+ export declare function parseJsonLine(line: string): Record<string, unknown> | null;
135
+ export declare function stderrExcerpt(stderr: string, max?: number): string;
136
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Model-CLI runtime adapters (local-bots US-003).
3
+ *
4
+ * One headless invocation per inbound DM, resumed by the CLI's own session id
5
+ * so context carries across turns and across restarts. The CLI runs under the
6
+ * OWNER's login (claude / codex / grok) — no Indigo-billed model keys.
7
+ */
8
+ export class RuntimeError extends Error {
9
+ stderrExcerpt;
10
+ exitCode;
11
+ /** False when the runtime process never started (missing binary, spawn error): safe to retry. */
12
+ started;
13
+ /** True when the turn was ended on purpose (owner stop), not by a model failure. */
14
+ aborted;
15
+ constructor(message, stderrExcerpt, exitCode, opts = {}) {
16
+ super(message);
17
+ this.stderrExcerpt = stderrExcerpt;
18
+ this.exitCode = exitCode;
19
+ this.name = "RuntimeError";
20
+ this.started = opts.started ?? true;
21
+ this.aborted = opts.aborted ?? false;
22
+ }
23
+ }
24
+ /**
25
+ * Turns have no time limit: a turn runs until the model process exits (the
26
+ * owner can still stop the bot). `HQ_BOT_TURN_TIMEOUT_MS` is an escape hatch;
27
+ * unset, empty, zero or invalid means unlimited.
28
+ */
29
+ export const TURN_TIMEOUT_ENV = "HQ_BOT_TURN_TIMEOUT_MS";
30
+ export function turnTimeoutMs(env = process.env) {
31
+ const raw = Number(env[TURN_TIMEOUT_ENV]);
32
+ return Number.isFinite(raw) && raw > 0 ? raw : undefined;
33
+ }
34
+ /** Parse one NDJSON line; null when it is not a JSON object. */
35
+ export function parseJsonLine(line) {
36
+ const t = line.trim();
37
+ if (!t.startsWith("{"))
38
+ return null;
39
+ try {
40
+ const v = JSON.parse(t);
41
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ export function stderrExcerpt(stderr, max = 400) {
48
+ const lines = stderr.split("\n").map((l) => l.trim()).filter(Boolean);
49
+ return lines.slice(-6).join(" | ").slice(0, max);
50
+ }
51
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * personal/workers/<name>/ scaffold for a local bot (local-bots US-005/US-006).
3
+ * Never overwrites an existing file (the bot edits its own memory).
4
+ */
5
+ import * as fs from "node:fs";
6
+ export interface ScaffoldResult {
7
+ /** personal/workers/<name> (absolute). */
8
+ dir: string;
9
+ /** The memory folder (absolute) — inside `dir` unless `memoryDir` was given. */
10
+ memoryDir: string;
11
+ created: string[];
12
+ kept: string[];
13
+ }
14
+ export interface ScaffoldOptions {
15
+ /**
16
+ * Memory folder override: hqRoot-relative or absolute (`--memory local`
17
+ * keeps it under ~/.hq/bots/<name>/memory, outside the synced tree).
18
+ * Default: personal/workers/<name>/memory.
19
+ */
20
+ memoryDir?: string;
21
+ }
22
+ export declare function renderWorkerYaml(name: string, agentUid: string, runtime: string, memoryDir?: string): string;
23
+ export declare function renderPersonaMd(name: string): string;
24
+ export declare function renderMemoryReadme(name: string): string;
25
+ type ScaffoldFs = Pick<typeof fs, "existsSync" | "mkdirSync" | "writeFileSync">;
26
+ /** Absolute memory folder for a scaffold: the override (relative to hqRoot unless absolute) or the default. */
27
+ export declare function resolveScaffoldMemoryDir(hqRoot: string, name: string, memoryDir?: string): string;
28
+ /**
29
+ * The memory folder + README only — the memory a bot keeps between
30
+ * conversations: personal/workers/<name>/memory/ by default, or the given
31
+ * `memoryDir` (absolute for local memory). Used on its own for worker-sourced
32
+ * bots, whose definition lives in the company tree and must not be written
33
+ * to. Never overwrites an existing file.
34
+ */
35
+ export declare function scaffoldBotMemory(hqRoot: string, name: string, io?: ScaffoldFs, opts?: ScaffoldOptions): ScaffoldResult;
36
+ export declare function scaffoldBotWorker(hqRoot: string, name: string, agentUid: string, runtime: string, io?: ScaffoldFs, opts?: ScaffoldOptions): ScaffoldResult;
37
+ export {};
38
+ //# sourceMappingURL=scaffold.d.ts.map