@indigoai-us/hq-cli 5.345.104 → 5.345.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/assets/bot-workers/setup/skills/add-a-pack.md +130 -0
- package/assets/bot-workers/setup/worker.yaml +3 -0
- package/dist/command-catalog.generated.d.ts +206 -0
- package/dist/command-catalog.generated.js +246 -0
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +20 -12
- package/dist/commands/daemon.js +385 -1
- package/dist/commands/integrations-connect.d.ts +6 -0
- package/dist/commands/integrations-connect.js +36 -0
- package/dist/commands/integrations-slack.d.ts +30 -0
- package/dist/commands/integrations-slack.js +81 -0
- package/dist/commands/integrations.js +2 -0
- package/dist/commands/logout.d.ts +6 -1
- package/dist/commands/logout.js +18 -2
- package/dist/commands/pack-install.d.ts +20 -1
- package/dist/commands/pack-install.js +80 -5
- package/dist/commands/pack-start-bot.d.ts +24 -0
- package/dist/commands/pack-start-bot.js +76 -0
- package/dist/commands/packs.d.ts +9 -0
- package/dist/commands/packs.js +26 -3
- package/dist/commands/pkg-install.js +11 -2
- package/dist/commands/secrets.d.ts +1 -5
- package/dist/commands/secrets.js +2 -92
- package/dist/fast-scaffold.d.ts +13 -0
- package/dist/fast-scaffold.js +136 -0
- package/dist/index.js +15 -1
- package/dist/lib/bot/config.d.ts +7 -0
- package/dist/lib/bot/daemon.js +2 -1
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/lean-settings.d.ts +16 -0
- package/dist/lib/bot/lean-settings.js +34 -0
- package/dist/lib/bot/pack-link.d.ts +66 -0
- package/dist/lib/bot/pack-link.js +153 -0
- package/dist/lib/bot/pack-setup-driver.d.ts +37 -0
- package/dist/lib/bot/pack-setup-driver.js +83 -0
- package/dist/lib/bot/pack-setup.d.ts +62 -0
- package/dist/lib/bot/pack-setup.js +103 -0
- package/dist/lib/bot/prompt.d.ts +8 -0
- package/dist/lib/bot/prompt.js +5 -0
- package/dist/lib/bot/run.d.ts +18 -2
- package/dist/lib/bot/run.js +163 -25
- package/dist/lib/bot/runtime/claude.d.ts +27 -2
- package/dist/lib/bot/runtime/claude.js +49 -4
- package/dist/lib/bot/runtime/types.d.ts +5 -0
- package/dist/lib/bot/runtime-failure.d.ts +18 -2
- package/dist/lib/bot/runtime-failure.js +21 -1
- package/dist/lib/bot/toolchain.d.ts +25 -0
- package/dist/lib/bot/toolchain.js +33 -0
- package/dist/lib/bot/worker-source.d.ts +1 -0
- package/dist/lib/bot/worker-source.js +7 -1
- package/dist/lib/daemon/catalog.d.ts +15 -0
- package/dist/lib/daemon/catalog.js +49 -0
- package/dist/lib/daemon/paths.d.ts +1 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/remote-control-forwarder.d.ts +14 -0
- package/dist/lib/daemon/remote-control-forwarder.js +104 -0
- package/dist/lib/daemon/remote-control-pools.d.ts +44 -0
- package/dist/lib/daemon/remote-control-pools.js +245 -0
- package/dist/lib/daemon/remote-control.d.ts +4 -0
- package/dist/lib/daemon/remote-control.js +17 -2
- package/dist/lib/daemon/run.js +126 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +5 -4
- package/dist/lib/doctor/fix/refresh-sync-progress.js +5 -4
- package/dist/lib/doctor/json-output.d.ts +5 -5
- package/dist/lib/doctor/json-output.js +2 -5
- package/dist/lib/packages/setup-block.d.ts +43 -0
- package/dist/lib/packages/setup-block.js +236 -0
- package/dist/lib/packages/setup-prompt.d.ts +34 -0
- package/dist/lib/packages/setup-prompt.js +128 -0
- package/dist/lib/packages/setup-takeover.d.ts +50 -0
- package/dist/lib/packages/setup-takeover.js +75 -0
- package/dist/lib/packages/start-bot.d.ts +47 -0
- package/dist/lib/packages/start-bot.js +66 -0
- package/dist/lib/session-company-lock.d.ts +7 -0
- package/dist/lib/session-company-lock.js +97 -0
- package/dist/types.d.ts +82 -0
- package/dist/utils/cognito-config.d.ts +5 -0
- package/dist/utils/cognito-config.js +14 -0
- package/dist/utils/cognito-revoke.d.ts +33 -0
- package/dist/utils/cognito-revoke.js +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bot whose worker belongs to an installed pack with a `setup:` block gets
|
|
3
|
+
* an auto-generated "Pack setup" section in its system prompt, and, when the
|
|
4
|
+
* pack asks for it and HQ's own setup is unfinished, a takeover section with
|
|
5
|
+
* HQ's setup worker skills (build contract section 3).
|
|
6
|
+
*
|
|
7
|
+
* scan-packages links a pack worker at `core/workers/public/<id>` ->
|
|
8
|
+
* `core/packages/<pack>/workers/<id>`, so the pack is found by resolving the
|
|
9
|
+
* worker folder's real path and checking whether it sits under
|
|
10
|
+
* `<hqRoot>/core/packages/<pack>/`.
|
|
11
|
+
*/
|
|
12
|
+
import type { PackSetup } from "../../types.js";
|
|
13
|
+
import { type WorkerSkillSource } from "./prompt.js";
|
|
14
|
+
export interface PackSetupResolveFs {
|
|
15
|
+
realpathSync: (p: string) => string;
|
|
16
|
+
existsSync: (p: string) => boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The installed pack (name + root) a worker folder belongs to, or null. */
|
|
19
|
+
export declare function findPackForWorker(hqRoot: string, workerDir: string, io?: PackSetupResolveFs): {
|
|
20
|
+
packName: string;
|
|
21
|
+
packRoot: string;
|
|
22
|
+
} | null;
|
|
23
|
+
export interface PackSetupPromptSections {
|
|
24
|
+
packName: string;
|
|
25
|
+
/** Absolute installed pack root. */
|
|
26
|
+
packRoot: string;
|
|
27
|
+
/** The validated `setup:` block. */
|
|
28
|
+
setup: PackSetup;
|
|
29
|
+
/** "## Finish HQ setup first", present only when the bot takes over HQ setup. */
|
|
30
|
+
takeover?: string;
|
|
31
|
+
/** "## Pack setup (...)". */
|
|
32
|
+
packSetup: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ResolvePackSetupPromptInput {
|
|
35
|
+
hqRoot: string;
|
|
36
|
+
/** hqRoot-relative worker folder (bot.json `workerDir`). */
|
|
37
|
+
workerDir: string;
|
|
38
|
+
/** Only worker-sourced bots can belong to a pack. */
|
|
39
|
+
workerSource?: "scaffold" | "worker";
|
|
40
|
+
/** Seams for tests. */
|
|
41
|
+
io?: PackSetupResolveFs;
|
|
42
|
+
readProgressNote?: () => {
|
|
43
|
+
path: string | null;
|
|
44
|
+
note: string | null;
|
|
45
|
+
};
|
|
46
|
+
setupWorkerSkills?: (hqRoot: string) => {
|
|
47
|
+
workerDir: string;
|
|
48
|
+
skills: WorkerSkillSource[];
|
|
49
|
+
} | null;
|
|
50
|
+
log?: (level: "info" | "warn", message: string) => void;
|
|
51
|
+
}
|
|
52
|
+
/** HQ's setup worker folder and skills, resolved like `hq bot create --worker setup`. */
|
|
53
|
+
export declare function loadSetupWorkerSkills(hqRoot: string): {
|
|
54
|
+
workerDir: string;
|
|
55
|
+
skills: WorkerSkillSource[];
|
|
56
|
+
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* The prompt sections for a pack bot, or null when the bot's worker is not a
|
|
59
|
+
* pack worker or its pack has no valid `setup:` block.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolvePackSetupPrompt(input: ResolvePackSetupPromptInput): PackSetupPromptSections | null;
|
|
62
|
+
//# sourceMappingURL=pack-setup.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bot whose worker belongs to an installed pack with a `setup:` block gets
|
|
3
|
+
* an auto-generated "Pack setup" section in its system prompt, and, when the
|
|
4
|
+
* pack asks for it and HQ's own setup is unfinished, a takeover section with
|
|
5
|
+
* HQ's setup worker skills (build contract section 3).
|
|
6
|
+
*
|
|
7
|
+
* scan-packages links a pack worker at `core/workers/public/<id>` ->
|
|
8
|
+
* `core/packages/<pack>/workers/<id>`, so the pack is found by resolving the
|
|
9
|
+
* worker folder's real path and checking whether it sits under
|
|
10
|
+
* `<hqRoot>/core/packages/<pack>/`.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { readInstalledPackSetup } from "../packages/setup-block.js";
|
|
15
|
+
import { renderPackSetupSection } from "../packages/setup-prompt.js";
|
|
16
|
+
import { decideTakeover, readHqSetupProgressNote, renderTakeoverSection } from "../packages/setup-takeover.js";
|
|
17
|
+
import { readPackManifest } from "../../utils/pack-contributions.js";
|
|
18
|
+
import { readPromptSources } from "./prompt.js";
|
|
19
|
+
import { resolveBotWorker } from "./worker-source.js";
|
|
20
|
+
import { SETUP_BOT_WORKER_ID } from "./config.js";
|
|
21
|
+
const defaultFs = {
|
|
22
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
23
|
+
existsSync: (p) => fs.existsSync(p),
|
|
24
|
+
};
|
|
25
|
+
/** The installed pack (name + root) a worker folder belongs to, or null. */
|
|
26
|
+
export function findPackForWorker(hqRoot, workerDir, io = defaultFs) {
|
|
27
|
+
let packagesRoot;
|
|
28
|
+
let workerReal;
|
|
29
|
+
try {
|
|
30
|
+
packagesRoot = io.realpathSync(path.join(hqRoot, "core", "packages"));
|
|
31
|
+
workerReal = io.realpathSync(path.isAbsolute(workerDir) ? workerDir : path.join(hqRoot, workerDir));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const rel = path.relative(packagesRoot, workerReal);
|
|
37
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
38
|
+
return null;
|
|
39
|
+
const packName = rel.split(path.sep)[0];
|
|
40
|
+
if (!packName || packName.startsWith("."))
|
|
41
|
+
return null;
|
|
42
|
+
return { packName, packRoot: path.join(packagesRoot, packName) };
|
|
43
|
+
}
|
|
44
|
+
/** HQ's setup worker folder and skills, resolved like `hq bot create --worker setup`. */
|
|
45
|
+
export function loadSetupWorkerSkills(hqRoot) {
|
|
46
|
+
try {
|
|
47
|
+
const worker = resolveBotWorker(hqRoot, SETUP_BOT_WORKER_ID);
|
|
48
|
+
const sources = readPromptSources(hqRoot, worker.path, undefined, { workerSource: "worker" });
|
|
49
|
+
return { workerDir: worker.path, skills: sources.worker?.skills ?? [] };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The prompt sections for a pack bot, or null when the bot's worker is not a
|
|
57
|
+
* pack worker or its pack has no valid `setup:` block.
|
|
58
|
+
*/
|
|
59
|
+
export function resolvePackSetupPrompt(input) {
|
|
60
|
+
if (input.workerSource !== "worker")
|
|
61
|
+
return null;
|
|
62
|
+
const log = input.log ?? (() => { });
|
|
63
|
+
const found = findPackForWorker(input.hqRoot, input.workerDir, input.io);
|
|
64
|
+
if (!found)
|
|
65
|
+
return null;
|
|
66
|
+
const { manifest, error } = readPackManifest(found.packRoot);
|
|
67
|
+
if (!manifest) {
|
|
68
|
+
log("warn", `worker ${input.workerDir} belongs to pack ${found.packName} but its package.yaml is unreadable (${error ?? "unknown"}); no pack setup section`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (manifest.setup === undefined)
|
|
72
|
+
return null;
|
|
73
|
+
const setup = readInstalledPackSetup(manifest, found.packRoot, input.io?.existsSync);
|
|
74
|
+
if (!setup) {
|
|
75
|
+
log("warn", `pack ${found.packName} has a setup: block that does not validate; no pack setup section`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const promptInput = { packName: found.packName, packRoot: found.packRoot, setup };
|
|
79
|
+
const sections = {
|
|
80
|
+
packName: found.packName,
|
|
81
|
+
packRoot: found.packRoot,
|
|
82
|
+
setup,
|
|
83
|
+
packSetup: renderPackSetupSection(promptInput),
|
|
84
|
+
};
|
|
85
|
+
if (setup.takeover_setup) {
|
|
86
|
+
const progress = (input.readProgressNote ?? readHqSetupProgressNote)();
|
|
87
|
+
if (decideTakeover({ takeoverSetup: true, progressNote: progress.note })) {
|
|
88
|
+
const setupWorker = (input.setupWorkerSkills ?? loadSetupWorkerSkills)(input.hqRoot);
|
|
89
|
+
sections.takeover = renderTakeoverSection({
|
|
90
|
+
packName: found.packName,
|
|
91
|
+
setupWorkerDir: setupWorker?.workerDir ?? path.join("core", "workers", SETUP_BOT_WORKER_ID),
|
|
92
|
+
skills: setupWorker?.skills ?? [],
|
|
93
|
+
progressNotePath: progress.path,
|
|
94
|
+
});
|
|
95
|
+
log("info", `pack ${found.packName}: HQ setup is not finished; this bot finishes it before the pack setup`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
log("info", `pack ${found.packName}: HQ setup is finished; going straight to the pack setup`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return sections;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=pack-setup.js.map
|
package/dist/lib/bot/prompt.d.ts
CHANGED
|
@@ -93,6 +93,14 @@ export interface BuildSystemPromptInput {
|
|
|
93
93
|
kind?: BotKind;
|
|
94
94
|
/** Company bots: the slugs of the companies the bot is a member of. */
|
|
95
95
|
companies?: string[];
|
|
96
|
+
/**
|
|
97
|
+
* Pack bot: the auto-generated "Pack setup" section (and the HQ setup
|
|
98
|
+
* takeover section when it applies), rendered by lib/bot/pack-setup.ts.
|
|
99
|
+
*/
|
|
100
|
+
packSetup?: {
|
|
101
|
+
takeover?: string;
|
|
102
|
+
packSetup: string;
|
|
103
|
+
} | null;
|
|
96
104
|
/** hqRoot-relative (default `${workerDir}/memory`) or absolute. */
|
|
97
105
|
memoryDir?: string;
|
|
98
106
|
}
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -373,6 +373,11 @@ export function buildSystemPrompt(input) {
|
|
|
373
373
|
parts.push(`## Persona (${path.join(input.workerDir, "persona.md")})\n\n${input.sources.persona.trim()}`);
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
+
if (input.packSetup) {
|
|
377
|
+
if (input.packSetup.takeover)
|
|
378
|
+
parts.push(input.packSetup.takeover);
|
|
379
|
+
parts.push(input.packSetup.packSetup);
|
|
380
|
+
}
|
|
376
381
|
if (input.sources.memory.length > 0) {
|
|
377
382
|
const blocks = input.sources.memory.map((m) => `### ${m.name}\n\n${m.body.trim()}`);
|
|
378
383
|
parts.push(`## Memory (${memoryDir}/)\n\n${blocks.join("\n\n")}`);
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -201,13 +201,25 @@ export declare const KICKOFF_MESSAGE_ID = "kickoff";
|
|
|
201
201
|
export declare function turnEnv(base: NodeJS.ProcessEnv, kind: BotKind, agentUid?: string, companySlug?: string): NodeJS.ProcessEnv;
|
|
202
202
|
/** The core worker HQ's setup bot runs. */
|
|
203
203
|
export declare const SETUP_WORKER_ID = "setup";
|
|
204
|
+
/**
|
|
205
|
+
* The executable a bot's turns will spawn, for the start-up log: an absolute
|
|
206
|
+
* path when the runtime found one (the HQ app toolchain, a system prefix, the
|
|
207
|
+
* Claude desktop app), the bare name when it relies on PATH, or "not found"
|
|
208
|
+
* when the bare name is on no PATH entry either.
|
|
209
|
+
*/
|
|
210
|
+
export declare function describeRuntimeBinary(runtime: BotRuntime, env: NodeJS.ProcessEnv, cwd: string): string;
|
|
204
211
|
/** The one message a person gets when a turn on their message did not finish. */
|
|
205
212
|
export declare function didNotFinishText(reason: string, progressPosted: boolean): string;
|
|
206
213
|
export { containsNoReplySentinel };
|
|
207
214
|
/** Grok emits is_error with this message when the model produces only reasoning tokens. */
|
|
208
215
|
export declare function isReasoningOnlyError(excerpt: string): boolean;
|
|
209
|
-
/**
|
|
210
|
-
|
|
216
|
+
/**
|
|
217
|
+
* The one note a person gets when the bot pauses after repeated failures. With
|
|
218
|
+
* `reason` (the plain failure text of the last turn) it is the summary that
|
|
219
|
+
* replaces the fifth per-turn line: one message for the run of failures, with
|
|
220
|
+
* what went wrong and the one next step.
|
|
221
|
+
*/
|
|
222
|
+
export declare function holdNoticeText(holdMs: number, messagePending: boolean, reason?: string): string;
|
|
211
223
|
/** Owner stop, `hq bot stop` or `hq bot restart`: all arrive as the same signal. */
|
|
212
224
|
export declare const STOPPED_BY_OWNER_REASON = "I was stopped before I was done";
|
|
213
225
|
export declare const RESTARTED_REASON = "I was restarted while I was working on it";
|
|
@@ -229,4 +241,8 @@ export interface BotHandle {
|
|
|
229
241
|
export declare function inboxLaneKey(item: InboxItem, config: Pick<BotConfig, "ownerUid" | "agentUid" | "name">): string;
|
|
230
242
|
export declare function preflightCreds(dir: string, config: BotConfig): string | null;
|
|
231
243
|
export declare function runBot(deps: BotRunDeps): Promise<BotHandle>;
|
|
244
|
+
/** True for a setup note the desktop app sent, not words the person typed. */
|
|
245
|
+
export declare function isAppSetupNote(text: string): boolean;
|
|
246
|
+
/** Put ahead of an app setup note: fold it in, and stay quiet unless it changes what to say. */
|
|
247
|
+
export declare const APP_SETUP_NOTE_TURN_NOTE: string;
|
|
232
248
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -63,10 +63,13 @@ import { containsNoReplySentinel, ProgressPoster, } from "./progress.js";
|
|
|
63
63
|
import { splitBody } from "./split.js";
|
|
64
64
|
import { createBotLogger } from "./log.js";
|
|
65
65
|
import { buildSystemPrompt, introDmText, readPromptSources } from "./prompt.js";
|
|
66
|
+
import { resolvePackSetupPrompt } from "./pack-setup.js";
|
|
67
|
+
import { writeLeanClaudeSettings } from "./lean-settings.js";
|
|
68
|
+
import { companyDisplayName, firstQuestionNote, questionDefault, questionMessage, readPackSetupState, writePackSetupState, } from "./pack-setup-driver.js";
|
|
66
69
|
import { loadSetupExplainer, patchSetupIntroState, readSetupIntroState, readSetupProgressIntro, replyOffersExplain, SETUP_EXPLAINED_NOTE, setupExplainDue, } from "./setup-explain.js";
|
|
67
70
|
import { detectCodingToolActivity, SIGNIFICANT_CODING_TOOL_ACTIVITY, } from "./coding-tool-activity.js";
|
|
68
71
|
import { setupKeptHereNote, setupOfferAnsweredInWordsNote, setupToolOfferAnswer, setupToolOfferText, setupWelcomeOpening, } from "./setup-tool-offer.js";
|
|
69
|
-
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled } from "./runtime/claude.js";
|
|
72
|
+
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled, onPath } from "./runtime/claude.js";
|
|
70
73
|
import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
|
|
71
74
|
import { botAttachmentsDir, dmSessionScope, isDmSessionScope } from "./paths.js";
|
|
72
75
|
import { appendDmHistory, dmHistoryHasModelThread, markDmHistoryModelTurn, readDmHistory, withRecentDmHistory } from "./dm-history.js";
|
|
@@ -78,7 +81,7 @@ import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOut
|
|
|
78
81
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
79
82
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
80
83
|
import { SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
|
|
81
|
-
import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, } from "./runtime-failure.js";
|
|
84
|
+
import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, withRetrySuggestion, } from "./runtime-failure.js";
|
|
82
85
|
import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
|
|
83
86
|
import { captureHeartbeatIdentityMismatch, identityMismatchMessage, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
|
|
84
87
|
import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
|
|
@@ -183,6 +186,24 @@ export function turnEnv(base, kind, agentUid, companySlug) {
|
|
|
183
186
|
}
|
|
184
187
|
/** The core worker HQ's setup bot runs. */
|
|
185
188
|
export const SETUP_WORKER_ID = SETUP_BOT_WORKER_ID;
|
|
189
|
+
/**
|
|
190
|
+
* The executable a bot's turns will spawn, for the start-up log: an absolute
|
|
191
|
+
* path when the runtime found one (the HQ app toolchain, a system prefix, the
|
|
192
|
+
* Claude desktop app), the bare name when it relies on PATH, or "not found"
|
|
193
|
+
* when the bare name is on no PATH entry either.
|
|
194
|
+
*/
|
|
195
|
+
export function describeRuntimeBinary(runtime, env, cwd) {
|
|
196
|
+
let binary;
|
|
197
|
+
try {
|
|
198
|
+
binary = runtime.resolveBinary?.(env, cwd, process.platform) ?? runtime.binary;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
binary = runtime.binary;
|
|
202
|
+
}
|
|
203
|
+
if (path.isAbsolute(binary))
|
|
204
|
+
return binary;
|
|
205
|
+
return onPath(binary, env) ? `${binary} (on PATH)` : `${binary} (not found on PATH; turns will fail as not-installed)`;
|
|
206
|
+
}
|
|
186
207
|
/** The one message a person gets when a turn on their message did not finish. */
|
|
187
208
|
export function didNotFinishText(reason, progressPosted) {
|
|
188
209
|
const r = reason.trim().replace(/[.\s]+$/, "");
|
|
@@ -194,10 +215,16 @@ export { containsNoReplySentinel };
|
|
|
194
215
|
export function isReasoningOnlyError(excerpt) {
|
|
195
216
|
return excerpt.includes("reasoning_only");
|
|
196
217
|
}
|
|
197
|
-
/**
|
|
198
|
-
|
|
218
|
+
/**
|
|
219
|
+
* The one note a person gets when the bot pauses after repeated failures. With
|
|
220
|
+
* `reason` (the plain failure text of the last turn) it is the summary that
|
|
221
|
+
* replaces the fifth per-turn line: one message for the run of failures, with
|
|
222
|
+
* what went wrong and the one next step.
|
|
223
|
+
*/
|
|
224
|
+
export function holdNoticeText(holdMs, messagePending, reason) {
|
|
199
225
|
const minutes = Math.max(1, Math.round(holdMs / 60_000));
|
|
200
|
-
|
|
226
|
+
const base = `I'm pausing for ${minutes} minute${minutes === 1 ? "" : "s"} after ${reason ? `${FAILURE_LIMIT} failed tries` : "a few failed tries"}, then I'll ${messagePending ? "pick your message up" : "pick up from your next message"}.`;
|
|
227
|
+
return reason ? `${base} ${reason}` : base;
|
|
201
228
|
}
|
|
202
229
|
/** Owner stop, `hq bot stop` or `hq bot restart`: all arrive as the same signal. */
|
|
203
230
|
export const STOPPED_BY_OWNER_REASON = "I was stopped before I was done";
|
|
@@ -459,6 +486,7 @@ export async function runBot(deps) {
|
|
|
459
486
|
// A restart is the owner's "try again" after signing in: start clean.
|
|
460
487
|
setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null, cliVersion });
|
|
461
488
|
log("info", `starting ${config.name} on hq-cli ${cliVersion} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
|
|
489
|
+
log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, config.companySlug), turnCwd)}`);
|
|
462
490
|
deps.cliUpdate?.startup(log);
|
|
463
491
|
try {
|
|
464
492
|
const record = await api.getAgent(config.agentUid);
|
|
@@ -479,8 +507,19 @@ export async function runBot(deps) {
|
|
|
479
507
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
480
508
|
}
|
|
481
509
|
// ── System prompt ──────────────────────────────────────────────────────────
|
|
510
|
+
// A pack worker's bot gets the pack's "Pack setup" section (and the HQ setup
|
|
511
|
+
// takeover when the pack asks for it and setup is unfinished).
|
|
512
|
+
// Resolved even under a prompt override: the instant pack setup reads it too.
|
|
513
|
+
let packSetup = null;
|
|
514
|
+
try {
|
|
515
|
+
packSetup = resolvePackSetupPrompt({ hqRoot: config.hqRoot, workerDir: config.workerDir, workerSource: config.workerSource, log });
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
log("warn", `pack setup section skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
519
|
+
}
|
|
482
520
|
const systemPrompt = deps.systemPromptOverride ??
|
|
483
521
|
buildSystemPrompt({
|
|
522
|
+
packSetup,
|
|
484
523
|
botName: config.name,
|
|
485
524
|
...(config.displayName ? { displayName: config.displayName } : {}),
|
|
486
525
|
agentUid: config.agentUid,
|
|
@@ -502,6 +541,16 @@ export async function runBot(deps) {
|
|
|
502
541
|
const systemPromptFile = path.join(dir, "system-prompt.md");
|
|
503
542
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
504
543
|
fs.writeFileSync(systemPromptFile, systemPrompt, { mode: 0o600 });
|
|
544
|
+
// A lean bot's Claude Code turns load only HQ's tool hooks (lean-settings.ts).
|
|
545
|
+
let leanSettingsFile = null;
|
|
546
|
+
if (config.lean && config.runtime === "claude") {
|
|
547
|
+
try {
|
|
548
|
+
leanSettingsFile = writeLeanClaudeSettings(dir, config.hqRoot);
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
log("warn", `lean settings not written; turns load the full settings: ${err instanceof Error ? err.message : String(err)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
505
554
|
if (hasPromotionHold(dir)) {
|
|
506
555
|
await finish("stopped", 0, "local bot held for cloud promotion");
|
|
507
556
|
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
@@ -580,7 +629,7 @@ export async function runBot(deps) {
|
|
|
580
629
|
*/
|
|
581
630
|
const modelHoldMs = Math.max(0, deps.modelHoldMs ?? FAILURE_WINDOW_MS);
|
|
582
631
|
let modelHoldUntil = 0;
|
|
583
|
-
const holdAfterFailures = async (excerpt, tell, messagePending = false) => {
|
|
632
|
+
const holdAfterFailures = async (excerpt, tell, messagePending = false, cls) => {
|
|
584
633
|
const until = now().getTime() + modelHoldMs;
|
|
585
634
|
modelHoldUntil = until;
|
|
586
635
|
failures.clear();
|
|
@@ -592,7 +641,7 @@ export async function runBot(deps) {
|
|
|
592
641
|
// nothing while their next messages wait out the hold.
|
|
593
642
|
if (tell) {
|
|
594
643
|
try {
|
|
595
|
-
await tell(holdNoticeText(modelHoldMs, messagePending));
|
|
644
|
+
await tell(holdNoticeText(modelHoldMs, messagePending, cls ? runtimeFailureText(cls, runtime.id) : undefined));
|
|
596
645
|
}
|
|
597
646
|
catch (err) {
|
|
598
647
|
log("warn", `could not say that turns are on hold: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -863,6 +912,7 @@ export async function runBot(deps) {
|
|
|
863
912
|
sessionId: session?.sessionId,
|
|
864
913
|
...(live.model ? { model: live.model } : {}),
|
|
865
914
|
...(live.effort ? { effort: live.effort } : {}),
|
|
915
|
+
...(leanSettingsFile ? { leanSettingsFile } : {}),
|
|
866
916
|
...bind,
|
|
867
917
|
...stream,
|
|
868
918
|
});
|
|
@@ -1046,6 +1096,12 @@ export async function runBot(deps) {
|
|
|
1046
1096
|
* A server that refuses the block (400) still gets the plain body.
|
|
1047
1097
|
*/
|
|
1048
1098
|
const postRepair = async (target, notice, rootEventId) => {
|
|
1099
|
+
if (target.kind === "dm" && !notice.richContent) {
|
|
1100
|
+
// No repair card for this class: a tap-to-answer Retry so the failure is
|
|
1101
|
+
// never a dead end in the person's DM (rooms get the plain line).
|
|
1102
|
+
await api.sendDm({ toPersonUid: target.peerUid, body: withRetrySuggestion(notice.text), audience: "human", ...(rootEventId ? { rootEventId } : {}) });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1049
1105
|
if (target.kind === "dm" && notice.richContent) {
|
|
1050
1106
|
const base = { toPersonUid: target.peerUid, body: notice.text, audience: "human", ...(rootEventId ? { rootEventId } : {}) };
|
|
1051
1107
|
try {
|
|
@@ -1400,12 +1456,56 @@ export async function runBot(deps) {
|
|
|
1400
1456
|
recordToolOffer(tool);
|
|
1401
1457
|
return true;
|
|
1402
1458
|
};
|
|
1459
|
+
// ── Instant first question (pack-setup-driver.ts) ─────────────────────────
|
|
1460
|
+
/** The one company this pack is set up in: the owner's only company with a folder here. */
|
|
1461
|
+
const resolvePackFirm = async () => {
|
|
1462
|
+
if (!deps.ownerContext)
|
|
1463
|
+
return null;
|
|
1464
|
+
const owner = await deps.ownerContext();
|
|
1465
|
+
if (owner.status !== "ok")
|
|
1466
|
+
return null;
|
|
1467
|
+
const local = owner.companies
|
|
1468
|
+
.map((c) => (c.companySlug ?? "").trim())
|
|
1469
|
+
.filter((slug, i, all) => slug && all.indexOf(slug) === i && fs.existsSync(path.join(config.hqRoot, "companies", slug)));
|
|
1470
|
+
return local.length === 1 ? local[0] : null;
|
|
1471
|
+
};
|
|
1472
|
+
/**
|
|
1473
|
+
* First start of a pack bot: send the first setup question from code. False
|
|
1474
|
+
* (run the kickoff model turn) when there is no pack setup, it already
|
|
1475
|
+
* started, or the owner has no single company here to set the pack up in.
|
|
1476
|
+
*/
|
|
1477
|
+
const startPackSetup = async () => {
|
|
1478
|
+
if (!packSetup || readPackSetupState(dir))
|
|
1479
|
+
return false;
|
|
1480
|
+
try {
|
|
1481
|
+
const firm = await resolvePackFirm();
|
|
1482
|
+
if (!firm) {
|
|
1483
|
+
log("info", "pack setup: no single company on this computer to set up in; the kickoff turn handles it");
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
const q = packSetup.setup.questions[0];
|
|
1487
|
+
const firmName = companyDisplayName(config.hqRoot, firm);
|
|
1488
|
+
await sendToTarget({ kind: "dm", peerUid: config.ownerUid }, questionMessage(q, questionDefault(q, { firmName })), undefined, "human");
|
|
1489
|
+
writePackSetupState(dir, { v: 1, pack: packSetup.packName, firm, askedFirst: q.id, modelNotePending: true, updatedAt: now().toISOString() });
|
|
1490
|
+
log("info", `pack setup for ${firm}: asked ${q.id} from code (no kickoff turn)`);
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
catch (err) {
|
|
1494
|
+
log("warn", `first pack setup question not sent from code (${err instanceof Error ? err.message : String(err)}); running the kickoff`);
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1403
1498
|
const runKickoff = async (introEventId) => {
|
|
1404
1499
|
const prompt = (config.kickoff ?? "").trim();
|
|
1405
1500
|
if (!prompt || stopping || state !== "running")
|
|
1406
1501
|
return;
|
|
1407
1502
|
if (isSetupBot && (await offerCodingTool()))
|
|
1408
1503
|
return;
|
|
1504
|
+
if (packSetup && (await startPackSetup())) {
|
|
1505
|
+
if (readBotConfig(dir)?.kickoffPendingSignIn)
|
|
1506
|
+
patchBotConfig(dir, { kickoffPendingSignIn: false });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1409
1509
|
const peerUid = config.ownerUid;
|
|
1410
1510
|
const tReceived = now().getTime();
|
|
1411
1511
|
log("info", `kickoff turn starting (${prompt.length} chars)`);
|
|
@@ -1462,6 +1562,13 @@ export async function runBot(deps) {
|
|
|
1462
1562
|
// The person sees one plain sentence and one next step; the raw runtime
|
|
1463
1563
|
// text is in the log (recordTurnFailure) and status.json only.
|
|
1464
1564
|
const cls = failureClass(outcome);
|
|
1565
|
+
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
1566
|
+
// The failure that hit the limit: one summary carries the reason and the pause.
|
|
1567
|
+
clearInflight(dir, KICKOFF_MESSAGE_ID);
|
|
1568
|
+
log("error", `kickoff turn failed (${count}/${FAILURE_LIMIT} in window); not retried`);
|
|
1569
|
+
await holdAfterFailures(outcome.excerpt, sendToPerson, false, cls);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1465
1572
|
const notice = outcome.aborted
|
|
1466
1573
|
? { cls, text: `${didNotFinishText(failureReason(outcome), poster.postedCount > 0)} Send me any message and I'll pick up from there.` }
|
|
1467
1574
|
: runtimeRepairNotice({
|
|
@@ -1479,9 +1586,6 @@ export async function runBot(deps) {
|
|
|
1479
1586
|
/* the marker stays: the next start says it did not finish */
|
|
1480
1587
|
}
|
|
1481
1588
|
log("error", `kickoff turn ${outcome.aborted ? "stopped by the owner" : `failed (${count}/${FAILURE_LIMIT} in window)`}; not retried`);
|
|
1482
|
-
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
1483
|
-
await holdAfterFailures(outcome.excerpt, sendToPerson);
|
|
1484
|
-
}
|
|
1485
1589
|
};
|
|
1486
1590
|
/**
|
|
1487
1591
|
* One fired job: a model turn on the job's own prompt, in the job's own
|
|
@@ -1629,12 +1733,14 @@ export async function runBot(deps) {
|
|
|
1629
1733
|
// The model never ran, so nothing happened outside this Mac: retry.
|
|
1630
1734
|
clearInflight(dir, id);
|
|
1631
1735
|
const { excerpt } = recordTurnFailure(outcome.error, target.sessionScope);
|
|
1736
|
+
const cls = classifyRuntimeError(outcome.error);
|
|
1632
1737
|
if (failures.exceeded(now().getTime())) {
|
|
1633
|
-
await holdAfterFailures(excerpt, sendToPerson, true);
|
|
1738
|
+
await holdAfterFailures(excerpt, sendToPerson, true, cls);
|
|
1634
1739
|
return;
|
|
1635
1740
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1741
|
+
// A missing tool does not come back between retries: say so now.
|
|
1742
|
+
if (attempt >= MAX_ATTEMPTS_PER_MESSAGE || cls === "not-installed") {
|
|
1743
|
+
const plain = runtimeFailureText(cls, runtime.id);
|
|
1638
1744
|
await sendToPerson(`I couldn't run this one. ${plain}`).catch(() => undefined);
|
|
1639
1745
|
await completeJobRun(jobId, id, { status: "failed", output: jobRunOutput(plain), disposition: `${runtime.id} could not be started` });
|
|
1640
1746
|
markReported("failed");
|
|
@@ -1674,7 +1780,7 @@ export async function runBot(deps) {
|
|
|
1674
1780
|
attempts.delete(id);
|
|
1675
1781
|
log("error", `scheduled run ${id} of job ${jobId} ${outcome.aborted ? "stopped by the owner" : "failed"} after it started; not re-run`);
|
|
1676
1782
|
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
1677
|
-
await holdAfterFailures(outcome.excerpt, sendToPerson);
|
|
1783
|
+
await holdAfterFailures(outcome.excerpt, sendToPerson, false, failureClass(outcome));
|
|
1678
1784
|
}
|
|
1679
1785
|
};
|
|
1680
1786
|
/**
|
|
@@ -1865,14 +1971,24 @@ export async function runBot(deps) {
|
|
|
1865
1971
|
const toolOffer = isSetupBot && ownerDm ? pendingToolOffer() : null;
|
|
1866
1972
|
const toolAnswer = toolOffer ? setupToolOfferAnswer(messageText, attachmentRefs.length) : null;
|
|
1867
1973
|
const kickoffPrompt = (config.kickoff ?? "").trim();
|
|
1974
|
+
// While (or right after) the app runs the pack setup, the model is told where it stands.
|
|
1975
|
+
const packState = packSetup && ownerDm ? readPackSetupState(dir) : null;
|
|
1976
|
+
const packTurnNote = packSetup && packState?.modelNotePending ? firstQuestionNote(packSetup.setup, packState, companyDisplayName(config.hqRoot, packState.firm)) : null;
|
|
1868
1977
|
const ownerText = toolOffer && toolAnswer === "kept_here" && kickoffPrompt
|
|
1869
1978
|
? `${setupKeptHereNote(toolOffer)}\n\n${kickoffPrompt}`
|
|
1870
1979
|
: toolOffer
|
|
1871
1980
|
? `${setupOfferAnsweredInWordsNote(toolOffer)}\n\n${text}`
|
|
1872
1981
|
: setupNote
|
|
1873
1982
|
? `${SETUP_EXPLAINED_NOTE}\n\n${text}`
|
|
1874
|
-
:
|
|
1875
|
-
|
|
1983
|
+
: packTurnNote
|
|
1984
|
+
? `${packTurnNote}\n\n${text}`
|
|
1985
|
+
: text;
|
|
1986
|
+
// A note the desktop app sends the setup bot as it finishes visual setup
|
|
1987
|
+
// steps. It often lands while the bot's previous question is still open;
|
|
1988
|
+
// answering every note re-posted that question (three times in a row on
|
|
1989
|
+
// 2026-10-12). Such a turn may stay quiet.
|
|
1990
|
+
const appSetupNote = isSetupBot && ownerDm && isAppSetupNote(messageText);
|
|
1991
|
+
const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? (appSetupNote ? `${APP_SETUP_NOTE_TURN_NOTE}\n\n${ownerText}` : ownerText) :
|
|
1876
1992
|
`[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
|
|
1877
1993
|
const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
|
|
1878
1994
|
const { sendProgress, sendToPerson, saved } = trackingSend(markerTarget, item.rootEventId);
|
|
@@ -1888,7 +2004,7 @@ export async function runBot(deps) {
|
|
|
1888
2004
|
tReceived,
|
|
1889
2005
|
sendProgress,
|
|
1890
2006
|
sendToPerson,
|
|
1891
|
-
silenceable: target.kind === "room" && !target.mentioned,
|
|
2007
|
+
silenceable: (target.kind === "room" && !target.mentioned) || appSetupNote,
|
|
1892
2008
|
...(readDirs.length ? { readDirs } : {}),
|
|
1893
2009
|
...(target.kind === "dm" ? { historyEventId: item.eventId ?? id } : {}),
|
|
1894
2010
|
...(target.kind === "room"
|
|
@@ -1917,6 +2033,11 @@ export async function runBot(deps) {
|
|
|
1917
2033
|
}
|
|
1918
2034
|
if (outcome.ok && ownerDm)
|
|
1919
2035
|
noteSetupReply(outcome.reply);
|
|
2036
|
+
if (outcome.ok && packState?.modelNotePending) {
|
|
2037
|
+
const latest = readPackSetupState(dir);
|
|
2038
|
+
if (latest)
|
|
2039
|
+
writePackSetupState(dir, { ...latest, modelNotePending: false, updatedAt: now().toISOString() });
|
|
2040
|
+
}
|
|
1920
2041
|
if (outcome.ok) {
|
|
1921
2042
|
// A no_reply sentinel or intentionally empty reply: ack silently, post nothing.
|
|
1922
2043
|
if (outcome.silent) {
|
|
@@ -1966,14 +2087,17 @@ export async function runBot(deps) {
|
|
|
1966
2087
|
// outside this Mac, so retrying is safe.
|
|
1967
2088
|
clearInflight(dir, id);
|
|
1968
2089
|
const { excerpt } = recordTurnFailure(outcome.error, sessionScope);
|
|
2090
|
+
const cls = classifyRuntimeError(outcome.error);
|
|
1969
2091
|
if (failures.exceeded(now().getTime())) {
|
|
1970
|
-
await holdAfterFailures(excerpt, sendToPerson, true);
|
|
2092
|
+
await holdAfterFailures(excerpt, sendToPerson, true, cls);
|
|
1971
2093
|
return;
|
|
1972
2094
|
}
|
|
1973
|
-
|
|
2095
|
+
// A missing tool does not come back between retries: tell the person on
|
|
2096
|
+
// the first attempt (with Retry) instead of after a silent minute.
|
|
2097
|
+
if (attempt >= MAX_ATTEMPTS_PER_MESSAGE || cls === "not-installed") {
|
|
1974
2098
|
try {
|
|
1975
2099
|
await postRepair(markerTarget, runtimeRepairNotice({
|
|
1976
|
-
cls
|
|
2100
|
+
cls,
|
|
1977
2101
|
runtime: runtime.id,
|
|
1978
2102
|
botName: config.name,
|
|
1979
2103
|
context: "message",
|
|
@@ -2010,15 +2134,21 @@ export async function runBot(deps) {
|
|
|
2010
2134
|
log("info", `unmentioned room turn ${id} failed with no post (${where}); acked silently`);
|
|
2011
2135
|
// Nobody asked the bot here, so the hold note goes to the owner's DM, not the room.
|
|
2012
2136
|
if (failures.exceeded(now().getTime())) {
|
|
2013
|
-
await holdAfterFailures(outcome.excerpt, (body) => sendToTarget({ kind: "dm", peerUid: config.ownerUid }, body, undefined, "human"));
|
|
2137
|
+
await holdAfterFailures(outcome.excerpt, (body) => sendToTarget({ kind: "dm", peerUid: config.ownerUid }, body, undefined, "human"), false, failureClass(outcome));
|
|
2014
2138
|
}
|
|
2015
2139
|
return;
|
|
2016
2140
|
}
|
|
2141
|
+
const holdNow = !outcome.aborted && failures.exceeded(now().getTime());
|
|
2017
2142
|
try {
|
|
2018
2143
|
// DM or @-mentioned room reasoning_only: the person asked directly, so
|
|
2019
2144
|
// still say we did not finish — but with a plain reason, never the raw
|
|
2020
2145
|
// internal error string.
|
|
2021
|
-
if (
|
|
2146
|
+
if (holdNow) {
|
|
2147
|
+
// The failure that hit the limit: one summary (reason, next step, the
|
|
2148
|
+
// pause) instead of a per-turn line followed by a pause note.
|
|
2149
|
+
await holdAfterFailures(outcome.excerpt, (body) => sendToPerson(body), false, failureClass(outcome));
|
|
2150
|
+
}
|
|
2151
|
+
else if (reasoningOnly || outcome.aborted) {
|
|
2022
2152
|
const reason = reasoningOnly ? "the model returned no answer" : failureReason(outcome);
|
|
2023
2153
|
await sendToPerson(didNotFinishText(reason, poster.postedCount > 0));
|
|
2024
2154
|
}
|
|
@@ -2044,9 +2174,6 @@ export async function runBot(deps) {
|
|
|
2044
2174
|
clearInflight(dir, id);
|
|
2045
2175
|
attempts.delete(id);
|
|
2046
2176
|
log("error", `turn on ${id} ${outcome.aborted ? "stopped by the owner" : "failed"} after it started; told the owner, not re-run`);
|
|
2047
|
-
if (!outcome.aborted && failures.exceeded(now().getTime())) {
|
|
2048
|
-
await holdAfterFailures(outcome.excerpt, sendToPerson);
|
|
2049
|
-
}
|
|
2050
2177
|
};
|
|
2051
2178
|
// The kickoff runs alongside the heartbeat, before the first inbox poll, so
|
|
2052
2179
|
// an owner message sent meanwhile is answered after it in the same session.
|
|
@@ -2259,4 +2386,15 @@ export async function runBot(deps) {
|
|
|
2259
2386
|
});
|
|
2260
2387
|
return { stop, done, status: readStatus, idle: () => pending.size === 0 };
|
|
2261
2388
|
}
|
|
2389
|
+
/** The prefix the HQ desktop app puts on the setup notes it sends the setup bot. */
|
|
2390
|
+
const APP_SETUP_NOTE_PREFIX = "Setup note from the HQ desktop app:";
|
|
2391
|
+
/** True for a setup note the desktop app sent, not words the person typed. */
|
|
2392
|
+
export function isAppSetupNote(text) {
|
|
2393
|
+
return text.trimStart().startsWith(APP_SETUP_NOTE_PREFIX);
|
|
2394
|
+
}
|
|
2395
|
+
/** Put ahead of an app setup note: fold it in, and stay quiet unless it changes what to say. */
|
|
2396
|
+
export const APP_SETUP_NOTE_TURN_NOTE = "[This message is a setup note from the HQ desktop app, not words from the person. Record what it settles. " +
|
|
2397
|
+
"If you already asked the person something that is still unanswered and this note does not change it, reply with exactly " +
|
|
2398
|
+
'{"action":"no_reply"} and nothing else, so nothing is posted. Never repeat a question you already asked. ' +
|
|
2399
|
+
"Write to the person only when the note changes what they need to know or answer next, in one or two sentences.]";
|
|
2262
2400
|
//# sourceMappingURL=run.js.map
|
|
@@ -45,6 +45,31 @@ export declare function onPath(name: string, env: NodeJS.ProcessEnv): boolean;
|
|
|
45
45
|
* skipped), matching the HQ desktop app's own lookup.
|
|
46
46
|
*/
|
|
47
47
|
export declare function bundledClaudeBin(homeDir?: string): string | null;
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Where a bot looks for `claude`, in order, outside the PATH walk. The HQ
|
|
50
|
+
* desktop app's toolchain comes first: on a clean Mac it is the only copy, and
|
|
51
|
+
* it is the one the app keeps updated. Then the usual install prefixes, so a
|
|
52
|
+
* bot under launchd (no shell profile) still finds a Homebrew or npm global
|
|
53
|
+
* install even when its PATH is thin.
|
|
54
|
+
*/
|
|
55
|
+
export declare const CLAUDE_SYSTEM_BIN_DIRS: readonly ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"];
|
|
56
|
+
/**
|
|
57
|
+
* The `claude` a bot spawns. Order (contract "Claude resolution for bots"):
|
|
58
|
+
*
|
|
59
|
+
* 1. `~/Library/Application Support/Indigo HQ/toolchain/npm-global/bin/claude`
|
|
60
|
+
* (the HQ app's own install; macOS only)
|
|
61
|
+
* 2. /usr/local/bin, /opt/homebrew/bin, /usr/bin
|
|
62
|
+
* 3. PATH (returned as the bare name, so the spawn uses the same lookup)
|
|
63
|
+
* 4. the Claude desktop app's bundled Claude Code
|
|
64
|
+
* 5. the bare name, so the failure reads "claude is not installed".
|
|
65
|
+
*
|
|
66
|
+
* Absolute paths come back for 1, 2 and 4 so the bot log can say which one
|
|
67
|
+
* ran.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveClaudeBinary(env: NodeJS.ProcessEnv, homeDir?: string, platform?: NodeJS.Platform, cwd?: string, systemBinDirs?: readonly string[]): string;
|
|
70
|
+
export interface ClaudeRuntimeOptions {
|
|
71
|
+
/** Install prefixes checked after the HQ app toolchain (test seam; default CLAUDE_SYSTEM_BIN_DIRS). */
|
|
72
|
+
systemBinDirs?: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
export declare function createClaudeRuntime(env?: NodeJS.ProcessEnv, opts?: ClaudeRuntimeOptions): BotRuntime;
|
|
50
75
|
//# sourceMappingURL=claude.d.ts.map
|